From 0fbc314fb4950c77529d6be6cca686d53160852e Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:21:02 -0500 Subject: [PATCH 01/52] [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 --- src/Core/Constants.cs | 1 - .../RotateUserAccountkeysCommand.cs | 12 +- .../ImportFeatures/ImportCiphersCommand.cs | 20 +- .../Vault/Repositories/ICipherRepository.cs | 22 -- .../Services/Implementations/CipherService.cs | 10 +- .../Vault/Repositories/CipherRepository.cs | 257 ------------------ .../Vault/Repositories/CipherRepository.cs | 41 --- .../ImportCiphersAsyncCommandTests.cs | 128 --------- .../Vault/Services/CipherServiceTests.cs | 53 ---- .../Repositories/CipherRepositoryTests.cs | 41 +-- 10 files changed, 10 insertions(+), 575 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1574c7f2ce..80b74877c5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -141,7 +141,6 @@ public static class FeatureFlagKeys public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; - public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs index 011fc2932f..91363abee8 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -25,7 +25,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IPasswordHasher _passwordHasher; - private readonly IFeatureService _featureService; /// /// Instantiates a new @@ -61,7 +60,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand _identityErrorDescriber = errors; _credentialRepository = credentialRepository; _passwordHasher = passwordHasher; - _featureService = featureService; } /// @@ -103,15 +101,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand List saveEncryptedDataActions = new(); if (model.Ciphers.Any()) { - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation_vNext(user.Id, model.Ciphers)); - } - else - { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); - } + saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); } if (model.Folders.Any()) diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index ce269bc68c..c7f7e3aff7 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -108,15 +108,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.CreateAsync_vNext(importingUserId, ciphers, newFolders); - } - else - { - await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); - } + await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); // push await _pushService.PushSyncVaultAsync(importingUserId); @@ -191,15 +183,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.CreateAsync_vNext(ciphers, newCollections, collectionCiphers, newCollectionUsers); - } - else - { - await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); - } + await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); // push await _pushService.PushSyncVaultAsync(importingUserId); diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 32acf3cbc9..94518bae2a 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -33,28 +33,12 @@ public interface ICipherRepository : IRepository Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers); - /// - /// - /// This version uses the bulk resource creation service to create the temp table. - /// - Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers); /// /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items. /// Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders); - /// - /// - /// This version uses the bulk resource creation service to create the temp tables. - /// - Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers); - /// - /// - /// This version uses the bulk resource creation service to create the temp tables. - /// - Task CreateAsync_vNext(IEnumerable ciphers, IEnumerable collections, - IEnumerable collectionCiphers, IEnumerable collectionUsers); Task SoftDeleteAsync(IEnumerable ids, Guid userId); Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task UnarchiveAsync(IEnumerable ids, Guid userId); @@ -92,10 +76,4 @@ public interface ICipherRepository : IRepository /// Task> GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId); - /// - /// - /// This version uses the bulk resource creation service to create the temp table. - /// - UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext(Guid userId, - IEnumerable ciphers); } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index ca6bacd55a..f132588e37 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -644,15 +644,7 @@ public class CipherService : ICipherService cipherIds.Add(cipher.Id); } - var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); - if (useBulkResourceCreationService) - { - await _cipherRepository.UpdateCiphersAsync_vNext(sharingUserId, cipherInfos.Select(c => c.cipher)); - } - else - { - await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher)); - } + await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher)); await _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId, organizationId, collectionIds); diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 4904574eee..48232ef484 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -13,7 +13,6 @@ using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.AdminConsole.Helpers; using Bit.Infrastructure.Dapper.Repositories; -using Bit.Infrastructure.Dapper.Vault.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -383,63 +382,6 @@ public class CipherRepository : Repository, ICipherRepository cmd.ExecuteNonQuery(); } - // Bulk copy data into temp table - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "#TempCipher"; - var ciphersTable = ciphers.ToDataTable(); - foreach (DataColumn col in ciphersTable.Columns) - { - bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); - } - - ciphersTable.PrimaryKey = new DataColumn[] { ciphersTable.Columns[0] }; - await bulkCopy.WriteToServerAsync(ciphersTable); - } - - // Update cipher table from temp table - var sql = @" - UPDATE - [dbo].[Cipher] - SET - [Data] = TC.[Data], - [Attachments] = TC.[Attachments], - [RevisionDate] = TC.[RevisionDate], - [Key] = TC.[Key] - FROM - [dbo].[Cipher] C - INNER JOIN - #TempCipher TC ON C.Id = TC.Id - WHERE - C.[UserId] = @UserId - - DROP TABLE #TempCipher"; - - await using (var cmd = new SqlCommand(sql, connection, transaction)) - { - cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; - cmd.ExecuteNonQuery(); - } - }; - } - - /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext( - Guid userId, IEnumerable ciphers) - { - return async (SqlConnection connection, SqlTransaction transaction) => - { - // Create temp table - var sqlCreateTemp = @" - SELECT TOP 0 * - INTO #TempCipher - FROM [dbo].[Cipher]"; - - await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) - { - cmd.ExecuteNonQuery(); - } - // Bulk copy data into temp table await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers); @@ -476,88 +418,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - // 1. Create temp tables to bulk copy into. - - var sqlCreateTemp = @" - SELECT TOP 0 * - INTO #TempCipher - FROM [dbo].[Cipher]"; - - using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) - { - cmd.ExecuteNonQuery(); - } - - // 2. Bulk copy into temp tables. - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "#TempCipher"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - // 3. Insert into real tables from temp tables and clean up. - - // Intentionally not including Favorites, Folders, and CreationDate - // since those are not meant to be bulk updated at this time - var sql = @" - UPDATE - [dbo].[Cipher] - SET - [UserId] = TC.[UserId], - [OrganizationId] = TC.[OrganizationId], - [Type] = TC.[Type], - [Data] = TC.[Data], - [Attachments] = TC.[Attachments], - [RevisionDate] = TC.[RevisionDate], - [DeletedDate] = TC.[DeletedDate], - [Key] = TC.[Key] - FROM - [dbo].[Cipher] C - INNER JOIN - #TempCipher TC ON C.Id = TC.Id - WHERE - C.[UserId] = @UserId - - DROP TABLE #TempCipher"; - - using (var cmd = new SqlCommand(sql, connection, transaction)) - { - cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; - cmd.ExecuteNonQuery(); - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = userId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); @@ -635,54 +495,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - if (folders.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Folder]"; - var dataTable = BuildFoldersTable(bulkCopy, folders); - bulkCopy.WriteToServer(dataTable); - } - } - - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Cipher]"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = userId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, IEnumerable folders) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); @@ -722,75 +534,6 @@ public class CipherRepository : Repository, ICipherRepository return; } - using (var connection = new SqlConnection(ConnectionString)) - { - connection.Open(); - - using (var transaction = connection.BeginTransaction()) - { - try - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Cipher]"; - var dataTable = BuildCiphersTable(bulkCopy, ciphers); - bulkCopy.WriteToServer(dataTable); - } - - if (collections.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[Collection]"; - var dataTable = BuildCollectionsTable(bulkCopy, collections); - bulkCopy.WriteToServer(dataTable); - } - } - - if (collectionCiphers.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[CollectionCipher]"; - var dataTable = BuildCollectionCiphersTable(bulkCopy, collectionCiphers); - bulkCopy.WriteToServer(dataTable); - } - } - - if (collectionUsers.Any()) - { - using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) - { - bulkCopy.DestinationTableName = "[dbo].[CollectionUser]"; - var dataTable = BuildCollectionUsersTable(bulkCopy, collectionUsers); - bulkCopy.WriteToServer(dataTable); - } - } - - await connection.ExecuteAsync( - $"[{Schema}].[User_BumpAccountRevisionDateByOrganizationId]", - new { OrganizationId = ciphers.First().OrganizationId }, - commandType: CommandType.StoredProcedure, transaction: transaction); - - transaction.Commit(); - } - catch - { - transaction.Rollback(); - throw; - } - } - } - } - - public async Task CreateAsync_vNext(IEnumerable ciphers, IEnumerable collections, - IEnumerable collectionCiphers, IEnumerable collectionUsers) - { - if (!ciphers.Any()) - { - return; - } - using (var connection = new SqlConnection(ConnectionString)) { connection.Open(); diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index d88f0e98bb..3c45afe530 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -168,16 +168,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular create method. - /// - public async Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, - IEnumerable folders) - { - await CreateAsync(userId, ciphers, folders); - } - public async Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, @@ -216,18 +206,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular create method. - /// - public async Task CreateAsync_vNext(IEnumerable ciphers, - IEnumerable collections, - IEnumerable collectionCiphers, - IEnumerable collectionUsers) - { - await CreateAsync(ciphers, collections, collectionCiphers, collectionUsers); - } - public async Task DeleteAsync(IEnumerable ids, Guid userId) { await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.HardDelete); @@ -986,15 +964,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular update method. - /// - public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers) - { - await UpdateCiphersAsync(userId, ciphers); - } - public async Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -1107,16 +1076,6 @@ public class CipherRepository : Repository - /// - /// EF does not use the bulk resource creation service, so we need to use the regular update method. - /// - public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation_vNext( - Guid userId, IEnumerable ciphers) - { - return UpdateForKeyRotation(userId, ciphers); - } - public async Task UpsertAsync(CipherDetails cipher) { if (cipher.Id.Equals(default)) diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 11f637d207..8c1cb1d5ff 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -53,38 +53,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_Success( - Guid importingUserId, - List ciphers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.OrganizationDataOwnership) - .Returns(false); - - sutProvider.GetDependency() - .GetManyByUserIdAsync(importingUserId) - .Returns(new List()); - - var folders = new List { new Folder { UserId = importingUserId } }; - - var folderRelationships = new List>(); - - // Act - await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .CreateAsync_vNext(importingUserId, ciphers, Arg.Any>()); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success( Guid importingUserId, @@ -117,42 +85,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoIndividualVaultAsync_WithBulkResourceCreationServiceEnabled_WithPolicyRequirementsEnabled_WithOrganizationDataOwnershipPolicyDisabled_Success( - Guid importingUserId, - List ciphers, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyRequirements) - .Returns(true); - - sutProvider.GetDependency() - .GetAsync(importingUserId) - .Returns(new OrganizationDataOwnershipPolicyRequirement( - OrganizationDataOwnershipState.Disabled, - [])); - - sutProvider.GetDependency() - .GetManyByUserIdAsync(importingUserId) - .Returns(new List()); - - var folders = new List { new Folder { UserId = importingUserId } }; - - var folderRelationships = new List>(); - - await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - - await sutProvider.GetDependency() - .Received(1) - .CreateAsync_vNext(importingUserId, ciphers, Arg.Any>()); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException( List folders, @@ -259,66 +191,6 @@ public class ImportCiphersAsyncCommandTests await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } - [Theory, BitAutoData] - public async Task ImportIntoOrganizationalVaultAsync_WithBulkResourceCreationServiceEnabled_Success( - Organization organization, - Guid importingUserId, - OrganizationUser importingOrganizationUser, - List collections, - List ciphers, - SutProvider sutProvider) - { - organization.MaxCollections = null; - importingOrganizationUser.OrganizationId = organization.Id; - - foreach (var collection in collections) - { - collection.OrganizationId = organization.Id; - } - - foreach (var cipher in ciphers) - { - cipher.OrganizationId = organization.Id; - } - - KeyValuePair[] collectionRelationships = { - new(0, 0), - new(1, 1), - new(2, 2) - }; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - sutProvider.GetDependency() - .GetByOrganizationAsync(organization.Id, importingUserId) - .Returns(importingOrganizationUser); - - // Set up a collection that already exists in the organization - sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organization.Id) - .Returns(new List { collections[0] }); - - await sutProvider.Sut.ImportIntoOrganizationalVaultAsync(collections, ciphers, collectionRelationships, importingUserId); - - await sutProvider.GetDependency().Received(1).CreateAsync_vNext( - ciphers, - Arg.Is>(cols => cols.Count() == collections.Count - 1 && - !cols.Any(c => c.Id == collections[0].Id) && // Check that the collection that already existed in the organization was not added - cols.All(c => collections.Any(x => c.Name == x.Name))), - Arg.Is>(c => c.Count() == ciphers.Count), - Arg.Is>(cus => - cus.Count() == collections.Count - 1 && - !cus.Any(cu => cu.CollectionId == collections[0].Id) && // Check that access was not added for the collection that already existed in the organization - cus.All(cu => cu.OrganizationUserId == importingOrganizationUser.Id && cu.Manage == true))); - await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); - } - [Theory, BitAutoData] public async Task ImportIntoOrganizationalVaultAsync_ThrowsBadRequestException( Organization organization, diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 44c86389e3..55db5a9143 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -674,32 +674,6 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } - [Theory] - [BitAutoData("")] - [BitAutoData("Correct Time")] - public async Task ShareManyAsync_CorrectRevisionDate_WithBulkResourceCreationServiceEnabled_Passes(string revisionDateString, - SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency().GetByIdAsync(organization.Id) - .Returns(new Organization - { - PlanType = PlanType.EnterpriseAnnually, - MaxStorageGb = 100 - }); - - var cipherInfos = ciphers.Select(c => (c, - string.IsNullOrEmpty(revisionDateString) ? null : (DateTime?)c.RevisionDate)); - var sharingUserId = ciphers.First().UserId.Value; - - await sutProvider.Sut.ShareManyAsync(cipherInfos, organization.Id, collectionIds, sharingUserId); - await sutProvider.GetDependency().Received(1).UpdateCiphersAsync_vNext(sharingUserId, - Arg.Is>(arg => !arg.Except(ciphers).Any())); - } - [Theory] [BitAutoData] public async Task RestoreAsync_UpdatesUserCipher(Guid restoringUserId, CipherDetails cipher, SutProvider sutProvider) @@ -1120,33 +1094,6 @@ public class CipherServiceTests Arg.Is>(arg => !arg.Except(ciphers).Any())); } - [Theory, BitAutoData] - public async Task ShareManyAsync_PaidOrgWithAttachment_WithBulkResourceCreationServiceEnabled_Passes(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation) - .Returns(true); - - sutProvider.GetDependency().GetByIdAsync(organizationId) - .Returns(new Organization - { - PlanType = PlanType.EnterpriseAnnually, - MaxStorageGb = 100 - }); - ciphers.FirstOrDefault().Attachments = - "{\"attachment1\":{\"Size\":\"250\",\"FileName\":\"superCoolFile\"," - + "\"Key\":\"superCoolFile\",\"ContainerName\":\"testContainer\",\"Validated\":false}}"; - - var cipherInfos = ciphers.Select(c => (c, - (DateTime?)c.RevisionDate)); - var sharingUserId = ciphers.First().UserId.Value; - - await sutProvider.Sut.ShareManyAsync(cipherInfos, organizationId, collectionIds, sharingUserId); - await sutProvider.GetDependency().Received(1).UpdateCiphersAsync_vNext(sharingUserId, - Arg.Is>(arg => !arg.Except(ciphers).Any())); - } - private class SaveDetailsAsyncDependencies { public CipherDetails CipherDetails { get; set; } diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 3a44453ed6..bb53bb1fd9 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -949,36 +949,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task UpdateCiphersAsync_Works(ICipherRepository cipherRepository, IUserRepository userRepository) - { - var user = await userRepository.CreateAsync(new User - { - Name = "Test User", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); - - var cipher1 = await CreatePersonalCipher(user, cipherRepository); - var cipher2 = await CreatePersonalCipher(user, cipherRepository); - - cipher1.Type = CipherType.SecureNote; - cipher2.Attachments = "new_attachments"; - - await cipherRepository.UpdateCiphersAsync(user.Id, [cipher1, cipher2]); - - var updatedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id); - var updatedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id); - - Assert.NotNull(updatedCipher1); - Assert.NotNull(updatedCipher2); - - Assert.Equal(CipherType.SecureNote, updatedCipher1.Type); - Assert.Equal("new_attachments", updatedCipher2.Attachments); - } - - [DatabaseTheory, DatabaseData] - public async Task CreateAsync_vNext_WithFolders_Works( + public async Task CreateAsync_WithFolders_Works( IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository) { // Arrange @@ -996,7 +967,7 @@ public class CipherRepositoryTests var cipher2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.SecureNote, UserId = user.Id, Data = "" }; // Act - await cipherRepository.CreateAsync_vNext( + await cipherRepository.CreateAsync( userId: user.Id, ciphers: [cipher1, cipher2], folders: [folder1, folder2]); @@ -1014,7 +985,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task CreateAsync_vNext_WithCollectionsAndUsers_Works( + public async Task CreateAsync_WithCollectionsAndUsers_Works( IOrganizationRepository orgRepository, IOrganizationUserRepository orgUserRepository, ICollectionRepository collectionRepository, @@ -1059,7 +1030,7 @@ public class CipherRepositoryTests }; // Act - await cipherRepository.CreateAsync_vNext( + await cipherRepository.CreateAsync( ciphers: [cipher], collections: [collection], collectionCiphers: [collectionCipher], @@ -1084,7 +1055,7 @@ public class CipherRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task UpdateCiphersAsync_vNext_Works( + public async Task UpdateCiphersAsync_Works( IUserRepository userRepository, ICipherRepository cipherRepository) { // Arrange @@ -1110,7 +1081,7 @@ public class CipherRepositoryTests c2.Attachments = expectedNewAttachments; // Act - await cipherRepository.UpdateCiphersAsync_vNext(user.Id, [c1, c2]); + await cipherRepository.UpdateCiphersAsync(user.Id, [c1, c2]); // Assert var updated1 = await cipherRepository.GetByIdAsync(c1.Id); From fe32e885c6fd5d2403a38bda77c4cff3950d9f35 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:47:29 -0400 Subject: [PATCH 02/52] [deps] Auth: Update Sustainsys.Saml2.AspNetCore2 to 2.11.0 (#6207) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/Sso.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/src/Sso/Sso.csproj b/bitwarden_license/src/Sso/Sso.csproj index 1b6b666ab1..2a1c14ae5a 100644 --- a/bitwarden_license/src/Sso/Sso.csproj +++ b/bitwarden_license/src/Sso/Sso.csproj @@ -10,7 +10,7 @@ - + From a6726d2e04b14b7cd35a8d55dcca83d7ab3c1719 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 9 Oct 2025 10:47:54 -0500 Subject: [PATCH 03/52] PM-26208 updated api endpoint (#6431) --- src/Billing/BillingSettings.cs | 9 +++ .../Controllers/FreshdeskController.cs | 80 +++++++++++-------- .../OnyxAnswerWithCitationRequestModel.cs | 51 ++++++++---- .../OnyxAnswerWithCitationResponseModel.cs | 33 -------- src/Billing/Models/OnyxResponseModel.cs | 15 ++++ src/Billing/appsettings.json | 8 +- .../Controllers/FreshdeskControllerTests.cs | 2 +- 7 files changed, 115 insertions(+), 83 deletions(-) delete mode 100644 src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs create mode 100644 src/Billing/Models/OnyxResponseModel.cs diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 32630e4a4a..3dc3e3e808 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -44,6 +44,15 @@ public class BillingSettings { public virtual string ApiKey { get; set; } public virtual string BaseUrl { get; set; } + public virtual string Path { get; set; } public virtual int PersonaId { get; set; } + public virtual bool UseAnswerWithCitationModels { get; set; } = true; + + public virtual SearchSettings SearchSettings { get; set; } = new SearchSettings(); + } + public class SearchSettings + { + public virtual string RunSearch { get; set; } = "auto"; // "always", "never", "auto" + public virtual bool RealTime { get; set; } = true; } } diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 66d4f47d92..38ed05cfdf 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Net.Http.Headers; using System.Reflection; using System.Text; @@ -35,7 +32,7 @@ public class FreshdeskController : Controller GlobalSettings globalSettings, IHttpClientFactory httpClientFactory) { - _billingSettings = billingSettings?.Value; + _billingSettings = billingSettings?.Value ?? throw new ArgumentNullException(nameof(billingSettings)); _userRepository = userRepository; _organizationRepository = organizationRepository; _logger = logger; @@ -101,7 +98,8 @@ public class FreshdeskController : Controller customFields[_billingSettings.FreshDesk.OrgFieldName] += $"\n{orgNote}"; } - var planName = GetAttribute(org.PlanType).Name.Split(" ").FirstOrDefault(); + var displayAttribute = GetAttribute(org.PlanType); + var planName = displayAttribute?.Name?.Split(" ").FirstOrDefault(); if (!string.IsNullOrWhiteSpace(planName)) { tags.Add(string.Format("Org: {0}", planName)); @@ -159,28 +157,22 @@ public class FreshdeskController : Controller return Ok(); } - // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId); - var onyxRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) - { - Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var (_, onyxJsonResponse) = await CallOnyxApi(onyxRequest); + // Get response from Onyx AI + var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); // the CallOnyxApi will return a null if we have an error response - if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) + if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) { _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequestModel), - JsonSerializer.Serialize(onyxJsonResponse)); + JsonSerializer.Serialize(onyxRequest), + JsonSerializer.Serialize(onyxResponse)); return Ok(); // return ok so we don't retry } // add the answer as a note to the ticket - await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + await AddAnswerNoteToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); return Ok(); } @@ -206,27 +198,21 @@ public class FreshdeskController : Controller } // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId); - var onyxRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) - { - Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")), - }; - var (_, onyxJsonResponse) = await CallOnyxApi(onyxRequest); + var (onyxRequest, onyxResponse) = await GetAnswerFromOnyx(model); // the CallOnyxApi will return a null if we have an error response - if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) + if (onyxResponse?.Answer == null || !string.IsNullOrEmpty(onyxResponse?.ErrorMsg)) { _logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ", JsonSerializer.Serialize(model), - JsonSerializer.Serialize(onyxRequestModel), - JsonSerializer.Serialize(onyxJsonResponse)); + JsonSerializer.Serialize(onyxRequest), + JsonSerializer.Serialize(onyxResponse)); return Ok(); // return ok so we don't retry } // add the reply to the ticket - await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + await AddReplyToTicketAsync(onyxResponse?.Answer ?? string.Empty, model.TicketId); return Ok(); } @@ -356,7 +342,32 @@ public class FreshdeskController : Controller return await CallFreshdeskApiAsync(request, retriedCount++); } - private async Task<(HttpResponseMessage, T)> CallOnyxApi(HttpRequestMessage request) + async Task<(OnyxRequestModel onyxRequest, OnyxResponseModel onyxResponse)> GetAnswerFromOnyx(FreshdeskOnyxAiWebhookModel model) + { + // TODO: remove the use of the deprecated answer-with-citation models after we are sure + if (_billingSettings.Onyx.UseAnswerWithCitationModels) + { + var onyxRequest = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); + var onyxAnswerWithCitationRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) + { + Content = JsonContent.Create(onyxRequest, mediaType: new MediaTypeHeaderValue("application/json")), + }; + var onyxResponse = await CallOnyxApi(onyxAnswerWithCitationRequest); + return (onyxRequest, onyxResponse); + } + + var request = new OnyxSendMessageSimpleApiRequestModel(model.TicketDescriptionText, _billingSettings.Onyx); + var onyxSimpleRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("{0}{1}", _billingSettings.Onyx.BaseUrl, _billingSettings.Onyx.Path)) + { + Content = JsonContent.Create(request, mediaType: new MediaTypeHeaderValue("application/json")), + }; + var onyxSimpleResponse = await CallOnyxApi(onyxSimpleRequest); + return (request, onyxSimpleResponse); + } + + private async Task CallOnyxApi(HttpRequestMessage request) where T : class, new() { var httpClient = _httpClientFactory.CreateClient("OnyxApi"); var response = await httpClient.SendAsync(request); @@ -365,7 +376,7 @@ public class FreshdeskController : Controller { _logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}", response.StatusCode, JsonSerializer.Serialize(response)); - return (null, default); + return new T(); } var responseStr = await response.Content.ReadAsStringAsync(); var responseJson = JsonSerializer.Deserialize(responseStr, options: new JsonSerializerOptions @@ -373,11 +384,12 @@ public class FreshdeskController : Controller PropertyNameCaseInsensitive = true, }); - return (response, responseJson); + return responseJson ?? new T(); } - private TAttribute GetAttribute(Enum enumValue) where TAttribute : Attribute + private TAttribute? GetAttribute(Enum enumValue) where TAttribute : Attribute { - return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute(); + var memberInfo = enumValue.GetType().GetMember(enumValue.ToString()).FirstOrDefault(); + return memberInfo != null ? memberInfo.GetCustomAttribute() : null; } } diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index ba3b89e297..9a753be4bc 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -1,35 +1,58 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - - -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using static Bit.Billing.BillingSettings; namespace Bit.Billing.Models; -public class OnyxAnswerWithCitationRequestModel +public class OnyxRequestModel { - [JsonPropertyName("messages")] - public List Messages { get; set; } - [JsonPropertyName("persona_id")] public int PersonaId { get; set; } = 1; [JsonPropertyName("retrieval_options")] - public RetrievalOptions RetrievalOptions { get; set; } + public RetrievalOptions RetrievalOptions { get; set; } = new RetrievalOptions(); - public OnyxAnswerWithCitationRequestModel(string message, int personaId = 1) + public OnyxRequestModel(OnyxSettings onyxSettings) + { + PersonaId = onyxSettings.PersonaId; + RetrievalOptions.RunSearch = onyxSettings.SearchSettings.RunSearch; + RetrievalOptions.RealTime = onyxSettings.SearchSettings.RealTime; + } +} + +/// +/// This is used with the onyx endpoint /query/answer-with-citation +/// which has been deprecated. This can be removed once later +/// +public class OnyxAnswerWithCitationRequestModel : OnyxRequestModel +{ + [JsonPropertyName("messages")] + public List Messages { get; set; } = new List(); + + public OnyxAnswerWithCitationRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) { message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); Messages = new List() { new Message() { MessageText = message } }; - RetrievalOptions = new RetrievalOptions(); - PersonaId = personaId; + } +} + +/// +/// This is used with the onyx endpoint /chat/send-message-simple-api +/// +public class OnyxSendMessageSimpleApiRequestModel : OnyxRequestModel +{ + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; + + public OnyxSendMessageSimpleApiRequestModel(string message, OnyxSettings onyxSettings) : base(onyxSettings) + { + Message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); } } public class Message { [JsonPropertyName("message")] - public string MessageText { get; set; } + public string MessageText { get; set; } = string.Empty; [JsonPropertyName("sender")] public string Sender { get; set; } = "user"; diff --git a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs deleted file mode 100644 index 5f67cd51d2..0000000000 --- a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Text.Json.Serialization; - -namespace Bit.Billing.Models; - -public class OnyxAnswerWithCitationResponseModel -{ - [JsonPropertyName("answer")] - public string Answer { get; set; } - - [JsonPropertyName("rephrase")] - public string Rephrase { get; set; } - - [JsonPropertyName("citations")] - public List Citations { get; set; } - - [JsonPropertyName("llm_selected_doc_indices")] - public List LlmSelectedDocIndices { get; set; } - - [JsonPropertyName("error_msg")] - public string ErrorMsg { get; set; } -} - -public class Citation -{ - [JsonPropertyName("citation_num")] - public int CitationNum { get; set; } - - [JsonPropertyName("document_id")] - public string DocumentId { get; set; } -} diff --git a/src/Billing/Models/OnyxResponseModel.cs b/src/Billing/Models/OnyxResponseModel.cs new file mode 100644 index 0000000000..96fa134c40 --- /dev/null +++ b/src/Billing/Models/OnyxResponseModel.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models; + +public class OnyxResponseModel +{ + [JsonPropertyName("answer")] + public string Answer { get; set; } = string.Empty; + + [JsonPropertyName("answer_citationless")] + public string AnswerCitationless { get; set; } = string.Empty; + + [JsonPropertyName("error_msg")] + public string ErrorMsg { get; set; } = string.Empty; +} diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 0074b5aafe..6c90c22686 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -80,7 +80,13 @@ "onyx": { "apiKey": "SECRET", "baseUrl": "https://cloud.onyx.app/api", - "personaId": 7 + "path": "/chat/send-message-simple-api", + "useAnswerWithCitationModels": true, + "personaId": 7, + "searchSettings": { + "runSearch": "always", + "realTime": true + } } } } diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 8fd0769a02..5c9199d29a 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -169,7 +169,7 @@ public class FreshdeskControllerTests [BitAutoData(WebhookKey)] public async Task PostWebhookOnyxAi_success( string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, - OnyxAnswerWithCitationResponseModel onyxResponse, + OnyxResponseModel onyxResponse, SutProvider sutProvider) { var billingSettings = sutProvider.GetDependency>().Value; From 712926996e6b6c6ecd51097ba4eef731f52636d2 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Thu, 9 Oct 2025 18:54:14 +0200 Subject: [PATCH 04/52] PM-26727 - First iteration of Claude Code Review tuning (#6435) * Sticky comments + pre-load specific project files + align prompts with clients repo --- .github/workflows/review-code.yml | 21 ++++++++++++++++++--- CLAUDE.md | 25 +++++++++++++++---------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index b49f5cec8f..83cbc3bb54 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -84,16 +84,18 @@ jobs: - name: Review with Claude Code if: steps.check_changes.outputs.vault_team_changes == 'true' - uses: anthropics/claude-code-action@a5528eec7426a4f0c9c1ac96018daa53ebd05bc4 # v1.0.7 + uses: anthropics/claude-code-action@ac1a3207f3f00b4a37e2f3a6f0935733c7c64651 # v1.0.11 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} track_progress: true + use_sticky_comment: true prompt: | REPO: ${{ github.repository }} PR NUMBER: ${{ github.event.pull_request.number }} TITLE: ${{ github.event.pull_request.title }} BODY: ${{ github.event.pull_request.body }} AUTHOR: ${{ github.event.pull_request.user.login }} + COMMIT: ${{ github.event.pull_request.head.sha }} Please review this pull request with a focus on: - Code quality and best practices @@ -103,7 +105,20 @@ jobs: Note: The PR branch is already checked out in the current working directory. - Provide detailed feedback using inline comments for specific issues. + Provide a comprehensive review including: + - Summary of changes since last review + - Critical issues found (be thorough) + - Suggested improvements (be thorough) + - Good practices observed (be concise - list only the most notable items without elaboration) + - Action items for the author + - Leverage collapsible
sections where appropriate for lengthy explanations or code snippets to enhance human readability + + When reviewing subsequent commits: + - Track status of previously identified issues (fixed/unfixed/reopened) + - Identify NEW problems introduced since last review + - Note if fixes introduced new issues + + IMPORTANT: Be comprehensive about issues and improvements. For good practices, be brief - just note what was done well without explaining why or praising excessively. claude_args: | - --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" + --allowedTools "mcp__github_comment__update_claude_comment,mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*)" diff --git a/CLAUDE.md b/CLAUDE.md index d07bd3f3e1..c1349e8c9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,24 +1,29 @@ # 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** edit: `/bin/`, `/obj/`, `/.git/`, `/.vs/`, `/packages/` which are generated files - **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 Context - -- **Architecture**: Feature and team-based organization -- **Framework**: .NET 8.0, ASP.NET Core -- **Database**: SQL Server primary, EF Core supports PostgreSQL, MySQL/MariaDB, SQLite -- **Testing**: xUnit, NSubstitute -- **Container**: Docker, Docker Compose, Kubernetes/Helm deployable - ## Project Structure - **Source Code**: `/src/` - Services and core infrastructure @@ -42,7 +47,7 @@ - **Database update**: `pwsh dev/migrate.ps1` - **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1` -## Code Review Checklist +## Development Workflow - Security impact assessed - xUnit tests added / updated From 34f5ffd981fb00568617666cfdfd67c370ad54e8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:20:28 -0500 Subject: [PATCH 05/52] [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 --- .../Queries/GetOrganizationWarningsQuery.cs | 32 +-- .../Services/OrganizationBillingService.cs | 16 +- .../Payment/Queries/HasPaymentMethodQuery.cs | 58 ++++ src/Core/Billing/Payment/Registrations.cs | 1 + .../GetOrganizationWarningsQueryTests.cs | 22 +- .../Queries/HasPaymentMethodQueryTests.cs | 264 ++++++++++++++++++ 6 files changed, 339 insertions(+), 54 deletions(-) create mode 100644 src/Core/Billing/Payment/Queries/HasPaymentMethodQuery.cs create mode 100644 test/Core.Test/Billing/Payment/Queries/HasPaymentMethodQueryTests.cs diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index f33814f1cf..887a6badf5 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -2,11 +2,11 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; @@ -30,8 +30,8 @@ public interface IGetOrganizationWarningsQuery public class GetOrganizationWarningsQuery( ICurrentContext currentContext, + IHasPaymentMethodQuery hasPaymentMethodQuery, IProviderRepository providerRepository, - ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IGetOrganizationWarningsQuery { @@ -81,15 +81,7 @@ public class GetOrganizationWarningsQuery( return null; } - var customer = subscription.Customer; - - var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(organization); - - var hasPaymentMethod = - !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || - !string.IsNullOrEmpty(customer.DefaultSourceId) || - hasUnverifiedBankAccount || - customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); + var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization); if (hasPaymentMethod) { @@ -287,22 +279,4 @@ public class GetOrganizationWarningsQuery( _ => null }; } - - private async Task HasUnverifiedBankAccountAsync( - Organization organization) - { - var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id); - - if (string.IsNullOrEmpty(setupIntentId)) - { - return false; - } - - var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions - { - Expand = ["payment_method"] - }); - - return setupIntent.IsUnverifiedBankAccount(); - } } diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index ce8a9a877b..36a618f799 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -6,6 +6,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; @@ -27,6 +28,7 @@ namespace Bit.Core.Billing.Organizations.Services; public class OrganizationBillingService( IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, + IHasPaymentMethodQuery hasPaymentMethodQuery, ILogger logger, IOrganizationRepository organizationRepository, IPricingClient pricingClient, @@ -43,7 +45,7 @@ public class OrganizationBillingService( ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType) : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup); - var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup); + var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup); if (subscription.Status is StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active) { @@ -120,8 +122,7 @@ public class OrganizationBillingService( orgOccupiedSeats.Total); } - public async Task - UpdatePaymentMethod( + public async Task UpdatePaymentMethod( Organization organization, TokenizedPaymentSource tokenizedPaymentSource, TaxInformation taxInformation) @@ -397,7 +398,7 @@ public class OrganizationBillingService( } private async Task CreateSubscriptionAsync( - Guid organizationId, + Organization organization, Customer customer, SubscriptionSetup subscriptionSetup) { @@ -465,7 +466,7 @@ public class OrganizationBillingService( Items = subscriptionItemOptionsList, Metadata = new Dictionary { - ["organizationId"] = organizationId.ToString(), + ["organizationId"] = organization.Id.ToString(), ["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) && subscriptionSetup.InitiationPath.Contains("trial from marketing website") ? "marketing-initiated" @@ -475,9 +476,10 @@ public class OrganizationBillingService( TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays }; + var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization); + // Only set trial_settings.end_behavior.missing_payment_method to "cancel" if there is no payment method - if (string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) && - !customer.Metadata.ContainsKey(BraintreeCustomerIdKey)) + if (!hasPaymentMethod) { subscriptionCreateOptions.TrialSettings = new SubscriptionTrialSettingsOptions { diff --git a/src/Core/Billing/Payment/Queries/HasPaymentMethodQuery.cs b/src/Core/Billing/Payment/Queries/HasPaymentMethodQuery.cs new file mode 100644 index 0000000000..ec77ee0712 --- /dev/null +++ b/src/Core/Billing/Payment/Queries/HasPaymentMethodQuery.cs @@ -0,0 +1,58 @@ +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Services; +using Stripe; + +namespace Bit.Core.Billing.Payment.Queries; + +using static StripeConstants; + +public interface IHasPaymentMethodQuery +{ + Task Run(ISubscriber subscriber); +} + +public class HasPaymentMethodQuery( + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : IHasPaymentMethodQuery +{ + public async Task Run(ISubscriber subscriber) + { + var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(subscriber); + + var customer = await subscriberService.GetCustomer(subscriber); + + if (customer == null) + { + return hasUnverifiedBankAccount; + } + + return + !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || + !string.IsNullOrEmpty(customer.DefaultSourceId) || + hasUnverifiedBankAccount || + customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); + } + + private async Task HasUnverifiedBankAccountAsync( + ISubscriber subscriber) + { + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id); + + if (string.IsNullOrEmpty(setupIntentId)) + { + return false; + } + + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }); + + return setupIntent.IsUnverifiedBankAccount(); + } +} diff --git a/src/Core/Billing/Payment/Registrations.cs b/src/Core/Billing/Payment/Registrations.cs index 478673d2fc..89d3778ccd 100644 --- a/src/Core/Billing/Payment/Registrations.cs +++ b/src/Core/Billing/Payment/Registrations.cs @@ -19,5 +19,6 @@ public static class Registrations services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 5234d500d1..96f9c1496e 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -2,10 +2,10 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; @@ -75,7 +75,7 @@ public class GetOrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); - sutProvider.GetDependency().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null); + sutProvider.GetDependency().Run(organization).Returns(false); var response = await sutProvider.Sut.Run(organization); @@ -86,12 +86,11 @@ public class GetOrganizationWarningsQueryTests } [Theory, BitAutoData] - public async Task Run_Has_FreeTrialWarning_WithUnverifiedBankAccount_NoWarning( + public async Task Run_Has_FreeTrialWarning_WithPaymentMethod_NoWarning( Organization organization, SutProvider sutProvider) { var now = DateTime.UtcNow; - const string setupIntentId = "setup_intent_id"; sutProvider.GetDependency() .GetSubscription(organization, Arg.Is(options => @@ -113,20 +112,7 @@ public class GetOrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); - sutProvider.GetDependency().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId); - sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is( - options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent - { - Status = "requires_action", - NextAction = new SetupIntentNextAction - { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() - }, - PaymentMethod = new PaymentMethod - { - UsBankAccount = new PaymentMethodUsBankAccount() - } - }); + sutProvider.GetDependency().Run(organization).Returns(true); var response = await sutProvider.Sut.Run(organization); diff --git a/test/Core.Test/Billing/Payment/Queries/HasPaymentMethodQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/HasPaymentMethodQueryTests.cs new file mode 100644 index 0000000000..c7ab0c17ff --- /dev/null +++ b/test/Core.Test/Billing/Payment/Queries/HasPaymentMethodQueryTests.cs @@ -0,0 +1,264 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Services; +using Bit.Core.Services; +using Bit.Core.Test.Billing.Extensions; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Queries; + +using static StripeConstants; + +public class HasPaymentMethodQueryTests +{ + private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly HasPaymentMethodQuery _query; + + public HasPaymentMethodQueryTests() + { + _query = new HasPaymentMethodQuery( + _setupIntentCache, + _stripeAdapter, + _subscriberService); + } + + [Fact] + public async Task Run_NoCustomer_ReturnsFalse() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + _subscriberService.GetCustomer(organization).ReturnsNull(); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns((string)null); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.False(hasPaymentMethod); + } + + [Fact] + public async Task Run_NoCustomer_WithUnverifiedBankAccount_ReturnsTrue() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + _subscriberService.GetCustomer(organization).ReturnsNull(); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123"); + + _stripeAdapter + .SetupIntentGet("seti_123", + Arg.Is(options => options.HasExpansions("payment_method"))) + .Returns(new SetupIntent + { + Status = "requires_action", + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + PaymentMethod = new PaymentMethod + { + UsBankAccount = new PaymentMethodUsBankAccount() + } + }); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.True(hasPaymentMethod); + } + + [Fact] + public async Task Run_NoPaymentMethod_ReturnsFalse() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.False(hasPaymentMethod); + } + + [Fact] + public async Task Run_HasDefaultPaymentMethodId_ReturnsTrue() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethodId = "pm_123" + }, + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.True(hasPaymentMethod); + } + + [Fact] + public async Task Run_HasDefaultSourceId_ReturnsTrue() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + DefaultSourceId = "card_123", + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.True(hasPaymentMethod); + } + + [Fact] + public async Task Run_HasUnverifiedBankAccount_ReturnsTrue() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123"); + + _stripeAdapter + .SetupIntentGet("seti_123", + Arg.Is(options => options.HasExpansions("payment_method"))) + .Returns(new SetupIntent + { + Status = "requires_action", + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + PaymentMethod = new PaymentMethod + { + UsBankAccount = new PaymentMethodUsBankAccount() + } + }); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.True(hasPaymentMethod); + } + + [Fact] + public async Task Run_HasBraintreeCustomerId_ReturnsTrue() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary + { + [MetadataKeys.BraintreeCustomerId] = "braintree_customer_id" + } + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.True(hasPaymentMethod); + } + + [Fact] + public async Task Run_NoSetupIntentId_ReturnsFalse() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns((string)null); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.False(hasPaymentMethod); + } + + [Fact] + public async Task Run_SetupIntentNotBankAccount_ReturnsFalse() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123"); + + _stripeAdapter + .SetupIntentGet("seti_123", + Arg.Is(options => options.HasExpansions("payment_method"))) + .Returns(new SetupIntent + { + PaymentMethod = new PaymentMethod + { + Type = "card" + }, + Status = "succeeded" + }); + + var hasPaymentMethod = await _query.Run(organization); + + Assert.False(hasPaymentMethod); + } +} From 3bef57259db4895ba75e3bd55920caadb193c2b6 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:50:07 -0500 Subject: [PATCH 06/52] [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 --- .gitignore | 1 + .../OrganizationBillingController.cs | 4 +- .../OrganizationBillingVNextController.cs | 19 + .../Responses/OrganizationMetadataResponse.cs | 31 -- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Models/OrganizationMetadata.cs | 18 - .../Queries/GetOrganizationMetadataQuery.cs | 95 +++++ .../Services/OrganizationBillingService.cs | 32 +- src/Core/Constants.cs | 1 + .../OrganizationBillingControllerTests.cs | 12 +- .../GetOrganizationMetadataQueryTests.cs | 369 ++++++++++++++++++ .../OrganizationBillingServiceTests.cs | 12 +- 12 files changed, 498 insertions(+), 97 deletions(-) delete mode 100644 src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs create mode 100644 src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs create mode 100644 test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs diff --git a/.gitignore b/.gitignore index 3b1f40e673..fd24c7713d 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,4 @@ bitwarden_license/src/Sso/Sso.zip /identity.json /api.json /api.public.json +.serena/ diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1d6bf51661..6e4cacc155 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -38,9 +38,7 @@ public class OrganizationBillingController( return Error.NotFound(); } - var response = OrganizationMetadataResponse.From(metadata); - - return TypedResults.Ok(response); + return TypedResults.Ok(metadata); } [HttpGet("history")] diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index 2f825f2cb9..64ec068a5e 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -4,6 +4,7 @@ using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requests.Subscriptions; using Bit.Api.Billing.Models.Requirements; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Organizations.Queries; @@ -25,6 +26,7 @@ public class OrganizationBillingVNextController( ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, + IGetOrganizationMetadataQuery getOrganizationMetadataQuery, IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IRestartSubscriptionCommand restartSubscriptionCommand, @@ -113,6 +115,23 @@ public class OrganizationBillingVNextController( return Handle(result); } + [Authorize] + [HttpGet("metadata")] + [RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)] + [InjectOrganization] + public async Task GetMetadataAsync( + [BindNever] Organization organization) + { + var metadata = await getOrganizationMetadataQuery.Run(organization); + + if (metadata == null) + { + return TypedResults.NotFound(); + } + + return TypedResults.Ok(metadata); + } + [Authorize] [HttpGet("warnings")] [InjectOrganization] diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs deleted file mode 100644 index a13f267c3b..0000000000 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.Billing.Organizations.Models; - -namespace Bit.Api.Billing.Models.Responses; - -public record OrganizationMetadataResponse( - bool IsEligibleForSelfHost, - bool IsManaged, - bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid, - bool HasSubscription, - bool HasOpenInvoice, - bool IsSubscriptionCanceled, - DateTime? InvoiceDueDate, - DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate, - int OrganizationOccupiedSeats) -{ - public static OrganizationMetadataResponse From(OrganizationMetadata metadata) - => new( - metadata.IsEligibleForSelfHost, - metadata.IsManaged, - metadata.IsOnSecretsManagerStandalone, - metadata.IsSubscriptionUnpaid, - metadata.HasSubscription, - metadata.HasOpenInvoice, - metadata.IsSubscriptionCanceled, - metadata.InvoiceDueDate, - metadata.InvoiceCreatedDate, - metadata.SubPeriodEndDate, - metadata.OrganizationOccupiedSeats); -} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 7aec422a4b..d6593f5365 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ public static class ServiceCollectionExtensions services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); services.AddPremiumCommands(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs index 2bcd213dbf..fedd0ad78c 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs @@ -1,28 +1,10 @@ namespace Bit.Core.Billing.Organizations.Models; public record OrganizationMetadata( - bool IsEligibleForSelfHost, - bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid, - bool HasSubscription, - bool HasOpenInvoice, - bool IsSubscriptionCanceled, - DateTime? InvoiceDueDate, - DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate, int OrganizationOccupiedSeats) { public static OrganizationMetadata Default => new OrganizationMetadata( false, - false, - false, - false, - false, - false, - false, - null, - null, - null, 0); } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs new file mode 100644 index 0000000000..63da0477a1 --- /dev/null +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs @@ -0,0 +1,95 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Stripe; + +namespace Bit.Core.Billing.Organizations.Queries; + +public interface IGetOrganizationMetadataQuery +{ + Task Run(Organization organization); +} + +public class GetOrganizationMetadataQuery( + IGlobalSettings globalSettings, + IOrganizationRepository organizationRepository, + IPricingClient pricingClient, + ISubscriberService subscriberService) : IGetOrganizationMetadataQuery +{ + public async Task Run(Organization organization) + { + if (organization == null) + { + return null; + } + + if (globalSettings.SelfHosted) + { + return OrganizationMetadata.Default; + } + + var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + return OrganizationMetadata.Default with + { + OrganizationOccupiedSeats = orgOccupiedSeats.Total + }; + } + + var customer = await subscriberService.GetCustomer(organization, + new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); + + var subscription = await subscriberService.GetSubscription(organization); + + if (customer == null || subscription == null) + { + return OrganizationMetadata.Default with + { + OrganizationOccupiedSeats = orgOccupiedSeats.Total + }; + } + + var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); + + return new OrganizationMetadata( + isOnSecretsManagerStandalone, + orgOccupiedSeats.Total); + } + + private async Task IsOnSecretsManagerStandalone( + Organization organization, + Customer? customer, + Subscription? subscription) + { + if (customer == null || subscription == null) + { + return false; + } + + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + if (!plan.SupportsSecretsManager) + { + return false; + } + + var hasCoupon = customer.Discount?.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone; + + if (!hasCoupon) + { + return false; + } + + var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId); + + var couponAppliesTo = customer.Discount?.Coupon?.AppliesTo?.Products; + + return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); + } +} diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 36a618f799..494a202c4b 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -74,16 +74,12 @@ public class OrganizationBillingService( return OrganizationMetadata.Default; } - var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); - - var isManaged = organization.Status == OrganizationStatusType.Managed; var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { return OrganizationMetadata.Default with { - IsEligibleForSelfHost = isEligibleForSelfHost, - IsManaged = isManaged, OrganizationOccupiedSeats = orgOccupiedSeats.Total }; } @@ -97,28 +93,14 @@ public class OrganizationBillingService( { return OrganizationMetadata.Default with { - IsEligibleForSelfHost = isEligibleForSelfHost, - IsManaged = isManaged + OrganizationOccupiedSeats = orgOccupiedSeats.Total }; } var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); - var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId) - ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) - : null; - return new OrganizationMetadata( - isEligibleForSelfHost, - isManaged, isOnSecretsManagerStandalone, - subscription.Status == StripeConstants.SubscriptionStatus.Unpaid, - true, - invoice?.Status == StripeConstants.InvoiceStatus.Open, - subscription.Status == StripeConstants.SubscriptionStatus.Canceled, - invoice?.DueDate, - invoice?.Created, - subscription.CurrentPeriodEnd, orgOccupiedSeats.Total); } @@ -536,16 +518,6 @@ public class OrganizationBillingService( return customer; } - private async Task IsEligibleForSelfHostAsync( - Organization organization) - { - var plans = await pricingClient.ListPlans(); - - var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); - - return eligibleSelfHostPlans.Contains(organization.PlanType); - } - private async Task IsOnSecretsManagerStandalone( Organization organization, Customer? customer, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 80b74877c5..a3c637ecb1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -179,6 +179,7 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; + public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure"; public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 51866320ee..d79bfde893 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -1,5 +1,4 @@ using Bit.Api.Billing.Controllers; -using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; @@ -53,19 +52,16 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0)); + .Returns(new OrganizationMetadata(true, 10)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); - Assert.IsType>(result); + Assert.IsType>(result); - var response = ((Ok)result).Value; + var response = ((Ok)result).Value; - Assert.True(response.IsEligibleForSelfHost); - Assert.True(response.IsManaged); Assert.True(response.IsOnSecretsManagerStandalone); - Assert.True(response.IsSubscriptionUnpaid); - Assert.True(response.HasSubscription); + Assert.Equal(10, response.OrganizationOccupiedSeats); } [Theory, BitAutoData] diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs new file mode 100644 index 0000000000..21081112d7 --- /dev/null +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs @@ -0,0 +1,369 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Organizations.Queries; + +[SutProviderCustomize] +public class GetOrganizationMetadataQueryTests +{ + [Theory, BitAutoData] + public async Task Run_NullOrganization_ReturnsNull( + SutProvider sutProvider) + { + var result = await sutProvider.Sut.Run(null); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task Run_SelfHosted_ReturnsDefault( + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency().SelfHosted.Returns(true); + + var result = await sutProvider.Sut.Run(organization); + + Assert.Equal(OrganizationMetadata.Default, result); + } + + [Theory, BitAutoData] + public async Task Run_NoGatewaySubscriptionId_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = null; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 10, Sponsored = 0 }); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(10, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_NullCustomer_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .ReturnsNull(); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(5, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_NullSubscription_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + + var customer = new Customer(); + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .ReturnsNull(); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(7, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_WithSecretsManagerStandaloneCoupon_ReturnsMetadataWithFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.EnterpriseAnnually; + + var productId = "product_123"; + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = [productId] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = productId + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.True(result.IsOnSecretsManagerStandalone); + Assert.Equal(15, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_WithoutSecretsManagerStandaloneCoupon_ReturnsMetadataWithoutFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.TeamsAnnually; + + var customer = new Customer + { + Discount = null + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = "product_123" + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(20, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_CouponDoesNotApplyToSubscriptionProducts_ReturnsFalseForStandaloneFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.EnterpriseAnnually; + + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = ["different_product_id"] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = "product_123" + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(12, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_PlanDoesNotSupportSecretsManager_ReturnsFalseForStandaloneFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.FamiliesAnnually; + + var productId = "product_123"; + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = [productId] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = productId + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(8, result.OrganizationOccupiedSeats); + } +} diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 7edc60a26a..77dce8101c 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -96,6 +96,10 @@ public class OrganizationBillingServiceTests sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 }); + var subscriberService = sutProvider.GetDependency(); // Set up subscriber service to return null for customer @@ -110,13 +114,7 @@ public class OrganizationBillingServiceTests Assert.NotNull(metadata); Assert.False(metadata!.IsOnSecretsManagerStandalone); - Assert.False(metadata.HasSubscription); - Assert.False(metadata.IsSubscriptionUnpaid); - Assert.False(metadata.HasOpenInvoice); - Assert.False(metadata.IsSubscriptionCanceled); - Assert.Null(metadata.InvoiceDueDate); - Assert.Null(metadata.InvoiceCreatedDate); - Assert.Null(metadata.SubPeriodEndDate); + Assert.Equal(1, metadata.OrganizationOccupiedSeats); } #endregion From b39e486e08d9e6032110efd791889a9982a6bf2b Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:14:27 -0400 Subject: [PATCH 07/52] 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 --------- Co-authored-by: Matt Bishop --- Directory.Build.props | 17 ------ src/Core/Utilities/AssemblyHelpers.cs | 54 +++++++++---------- .../Utilities/AssemblyHelpersTests.cs | 18 +++++++ 3 files changed, 45 insertions(+), 44 deletions(-) create mode 100644 test/Core.Test/Utilities/AssemblyHelpersTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index 76f35e297e..4aa72f3e81 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,8 +7,6 @@ Bit.$(MSBuildProjectName) enable - false - true annotations enable @@ -32,19 +30,4 @@ 4.18.1 - - - - - - - - - - - <_Parameter1>GitHash - <_Parameter2>$(SourceRevisionId) - - - \ No newline at end of file diff --git a/src/Core/Utilities/AssemblyHelpers.cs b/src/Core/Utilities/AssemblyHelpers.cs index 0cc01efdf3..03f7ff986d 100644 --- a/src/Core/Utilities/AssemblyHelpers.cs +++ b/src/Core/Utilities/AssemblyHelpers.cs @@ -1,46 +1,46 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - +using System.Diagnostics; using System.Reflection; namespace Bit.Core.Utilities; public static class AssemblyHelpers { - private static readonly IEnumerable _assemblyMetadataAttributes; - private static readonly AssemblyInformationalVersionAttribute _assemblyInformationalVersionAttributes; - private const string GIT_HASH_ASSEMBLY_KEY = "GitHash"; - private static string _version; - private static string _gitHash; + private static string? _version; + private static string? _gitHash; static AssemblyHelpers() { - _assemblyMetadataAttributes = Assembly.GetEntryAssembly().GetCustomAttributes(); - _assemblyInformationalVersionAttributes = Assembly.GetEntryAssembly().GetCustomAttribute(); - } - - public static string GetVersion() - { - if (string.IsNullOrWhiteSpace(_version)) + var assemblyInformationalVersionAttribute = typeof(AssemblyHelpers).Assembly.GetCustomAttribute(); + if (assemblyInformationalVersionAttribute == null) { - _version = _assemblyInformationalVersionAttributes.InformationalVersion; + Debug.Fail("The AssemblyInformationalVersionAttribute is expected to exist in this assembly, possibly its generation was turned off."); + return; } + var informationalVersion = assemblyInformationalVersionAttribute.InformationalVersion.AsSpan(); + + if (!informationalVersion.TrySplitBy('+', out var version, out var gitHash)) + { + // Treat the whole thing as the version + _version = informationalVersion.ToString(); + return; + } + + _version = version.ToString(); + if (gitHash.Length < 8) + { + return; + } + _gitHash = gitHash[..8].ToString(); + } + + public static string? GetVersion() + { return _version; } - public static string GetGitHash() + public static string? GetGitHash() { - if (string.IsNullOrWhiteSpace(_gitHash)) - { - try - { - _gitHash = _assemblyMetadataAttributes.Where(i => i.Key == GIT_HASH_ASSEMBLY_KEY).First().Value; - } - catch (Exception) - { } - } - return _gitHash; } } diff --git a/test/Core.Test/Utilities/AssemblyHelpersTests.cs b/test/Core.Test/Utilities/AssemblyHelpersTests.cs new file mode 100644 index 0000000000..463deb54a1 --- /dev/null +++ b/test/Core.Test/Utilities/AssemblyHelpersTests.cs @@ -0,0 +1,18 @@ +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class AssemblyHelpersTests +{ + [Fact] + public void ReturnsValidVersionAndGitHash() + { + var version = AssemblyHelpers.GetVersion(); + _ = Version.Parse(version); + + var gitHash = AssemblyHelpers.GetGitHash(); + Assert.NotNull(gitHash); + Assert.Equal(8, gitHash.Length); + } +} From c9970a0782c21175859784cb95cac2559a72fce0 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:19:45 -0500 Subject: [PATCH 08/52] Resolve tax estimation for Families scenarios (#6437) --- .../Commands/PreviewOrganizationTaxCommand.cs | 11 +- .../PreviewOrganizationTaxCommandTests.cs | 154 +++++++++++++++++- 2 files changed, 154 insertions(+), 11 deletions(-) diff --git a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs index 041e9bdbad..77bbe655c4 100644 --- a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs +++ b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs @@ -135,6 +135,8 @@ public class PreviewOrganizationTaxCommand( var newPlan = await pricingClient.GetPlanOrThrow(planChange.PlanType); + var quantity = newPlan.HasNonSeatBasedPasswordManagerPlan() ? 1 : 2; + var items = new List { new () @@ -142,7 +144,7 @@ public class PreviewOrganizationTaxCommand( Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId, - Quantity = 2 + Quantity = quantity } }; @@ -194,12 +196,17 @@ public class PreviewOrganizationTaxCommand( ? currentPlan.PasswordManager.StripePlanId : currentPlan.PasswordManager.StripeSeatPlanId]; + var quantity = currentPlan.HasNonSeatBasedPasswordManagerPlan() && + !newPlan.HasNonSeatBasedPasswordManagerPlan() + ? (long)organization.Seats! + : passwordManagerSeats.Quantity; + items.Add(new InvoiceSubscriptionDetailsItemOptions { Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId, - Quantity = passwordManagerSeats.Quantity + Quantity = quantity }); var hasStorage = diff --git a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs index 8e3cd5a0fa..08c3d9cf18 100644 --- a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs @@ -474,7 +474,149 @@ public class PreviewOrganizationTaxCommandTests options.CustomerDetails.TaxExempt == TaxExempt.None && options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" && - options.SubscriptionDetails.Items[0].Quantity == 2 && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_FamiliesOrganizationToTeams_UsesOrganizationSeats() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.FamiliesAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123", + UseSecretsManager = false, + Seats = 6 + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Teams, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "10012" + }; + + var currentPlan = new FamiliesPlan(); + var newPlan = new TeamsPlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan); + + var subscriptionItems = new List + { + new() { Price = new Price { Id = "2020-families-org-annually" }, Quantity = 1 } + }; + + var subscription = new Subscription + { + Id = "sub_test123", + Items = new StripeList { Data = subscriptionItems }, + Customer = new Customer { Discount = null } + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 900, + Total = 9900 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, planChange, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(9.00m, tax); + Assert.Equal(99.00m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "10012" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-annually" && + options.SubscriptionDetails.Items[0].Quantity == 6 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_FamiliesOrganizationToEnterprise_UsesOrganizationSeats() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.FamiliesAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123", + UseSecretsManager = false, + Seats = 6 + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "10012" + }; + + var currentPlan = new FamiliesPlan(); + var newPlan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan); + + var subscriptionItems = new List + { + new() { Price = new Price { Id = "2020-families-org-annually" }, Quantity = 1 } + }; + + var subscription = new Subscription + { + Id = "sub_test123", + Items = new StripeList { Data = subscriptionItems }, + Customer = new Customer { Discount = null } + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 1200, + Total = 13200 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, planChange, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(12.00m, tax); + Assert.Equal(132.00m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "10012" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" && + options.SubscriptionDetails.Items[0].Quantity == 6 && options.Coupon == null)); } @@ -956,10 +1098,7 @@ public class PreviewOrganizationTaxCommandTests Discount = null, TaxIds = new StripeList { - Data = new List - { - new() { Type = "gb_vat", Value = "GB123456789" } - } + Data = [new TaxId { Type = "gb_vat", Value = "GB123456789" }] } }; @@ -1040,10 +1179,7 @@ public class PreviewOrganizationTaxCommandTests }, TaxIds = new StripeList { - Data = new List - { - new() { Type = TaxIdType.SpanishNIF, Value = "12345678Z" } - } + Data = [new TaxId { Type = TaxIdType.SpanishNIF, Value = "12345678Z" }] } }; From 3272586e312f6045e5e49649f4da4cb217197369 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:06:58 -0500 Subject: [PATCH 09/52] Revert "[PM-25379] Refactor org metadata (#6418)" (#6439) This reverts commit 3bef57259db4895ba75e3bd55920caadb193c2b6. --- .gitignore | 1 - .../OrganizationBillingController.cs | 4 +- .../OrganizationBillingVNextController.cs | 19 - .../Responses/OrganizationMetadataResponse.cs | 31 ++ .../Extensions/ServiceCollectionExtensions.cs | 1 - .../Models/OrganizationMetadata.cs | 18 + .../Queries/GetOrganizationMetadataQuery.cs | 95 ----- .../Services/OrganizationBillingService.cs | 32 +- src/Core/Constants.cs | 1 - .../OrganizationBillingControllerTests.cs | 12 +- .../GetOrganizationMetadataQueryTests.cs | 369 ------------------ .../OrganizationBillingServiceTests.cs | 12 +- 12 files changed, 97 insertions(+), 498 deletions(-) create mode 100644 src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs delete mode 100644 src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs delete mode 100644 test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs diff --git a/.gitignore b/.gitignore index fd24c7713d..3b1f40e673 100644 --- a/.gitignore +++ b/.gitignore @@ -231,4 +231,3 @@ bitwarden_license/src/Sso/Sso.zip /identity.json /api.json /api.public.json -.serena/ diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 6e4cacc155..1d6bf51661 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -38,7 +38,9 @@ public class OrganizationBillingController( return Error.NotFound(); } - return TypedResults.Ok(metadata); + var response = OrganizationMetadataResponse.From(metadata); + + return TypedResults.Ok(response); } [HttpGet("history")] diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index 64ec068a5e..2f825f2cb9 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -4,7 +4,6 @@ using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requests.Subscriptions; using Bit.Api.Billing.Models.Requirements; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Organizations.Queries; @@ -26,7 +25,6 @@ public class OrganizationBillingVNextController( ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, - IGetOrganizationMetadataQuery getOrganizationMetadataQuery, IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IRestartSubscriptionCommand restartSubscriptionCommand, @@ -115,23 +113,6 @@ public class OrganizationBillingVNextController( return Handle(result); } - [Authorize] - [HttpGet("metadata")] - [RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)] - [InjectOrganization] - public async Task GetMetadataAsync( - [BindNever] Organization organization) - { - var metadata = await getOrganizationMetadataQuery.Run(organization); - - if (metadata == null) - { - return TypedResults.NotFound(); - } - - return TypedResults.Ok(metadata); - } - [Authorize] [HttpGet("warnings")] [InjectOrganization] diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs new file mode 100644 index 0000000000..a13f267c3b --- /dev/null +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -0,0 +1,31 @@ +using Bit.Core.Billing.Organizations.Models; + +namespace Bit.Api.Billing.Models.Responses; + +public record OrganizationMetadataResponse( + bool IsEligibleForSelfHost, + bool IsManaged, + bool IsOnSecretsManagerStandalone, + bool IsSubscriptionUnpaid, + bool HasSubscription, + bool HasOpenInvoice, + bool IsSubscriptionCanceled, + DateTime? InvoiceDueDate, + DateTime? InvoiceCreatedDate, + DateTime? SubPeriodEndDate, + int OrganizationOccupiedSeats) +{ + public static OrganizationMetadataResponse From(OrganizationMetadata metadata) + => new( + metadata.IsEligibleForSelfHost, + metadata.IsManaged, + metadata.IsOnSecretsManagerStandalone, + metadata.IsSubscriptionUnpaid, + metadata.HasSubscription, + metadata.HasOpenInvoice, + metadata.IsSubscriptionCanceled, + metadata.InvoiceDueDate, + metadata.InvoiceCreatedDate, + metadata.SubPeriodEndDate, + metadata.OrganizationOccupiedSeats); +} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index d6593f5365..7aec422a4b 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -31,7 +31,6 @@ public static class ServiceCollectionExtensions services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); services.AddPremiumCommands(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs index fedd0ad78c..2bcd213dbf 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs @@ -1,10 +1,28 @@ namespace Bit.Core.Billing.Organizations.Models; public record OrganizationMetadata( + bool IsEligibleForSelfHost, + bool IsManaged, bool IsOnSecretsManagerStandalone, + bool IsSubscriptionUnpaid, + bool HasSubscription, + bool HasOpenInvoice, + bool IsSubscriptionCanceled, + DateTime? InvoiceDueDate, + DateTime? InvoiceCreatedDate, + DateTime? SubPeriodEndDate, int OrganizationOccupiedSeats) { public static OrganizationMetadata Default => new OrganizationMetadata( false, + false, + false, + false, + false, + false, + false, + null, + null, + null, 0); } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs deleted file mode 100644 index 63da0477a1..0000000000 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs +++ /dev/null @@ -1,95 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Organizations.Models; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Stripe; - -namespace Bit.Core.Billing.Organizations.Queries; - -public interface IGetOrganizationMetadataQuery -{ - Task Run(Organization organization); -} - -public class GetOrganizationMetadataQuery( - IGlobalSettings globalSettings, - IOrganizationRepository organizationRepository, - IPricingClient pricingClient, - ISubscriberService subscriberService) : IGetOrganizationMetadataQuery -{ - public async Task Run(Organization organization) - { - if (organization == null) - { - return null; - } - - if (globalSettings.SelfHosted) - { - return OrganizationMetadata.Default; - } - - var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); - - if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) - { - return OrganizationMetadata.Default with - { - OrganizationOccupiedSeats = orgOccupiedSeats.Total - }; - } - - var customer = await subscriberService.GetCustomer(organization, - new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); - - var subscription = await subscriberService.GetSubscription(organization); - - if (customer == null || subscription == null) - { - return OrganizationMetadata.Default with - { - OrganizationOccupiedSeats = orgOccupiedSeats.Total - }; - } - - var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); - - return new OrganizationMetadata( - isOnSecretsManagerStandalone, - orgOccupiedSeats.Total); - } - - private async Task IsOnSecretsManagerStandalone( - Organization organization, - Customer? customer, - Subscription? subscription) - { - if (customer == null || subscription == null) - { - return false; - } - - var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); - - if (!plan.SupportsSecretsManager) - { - return false; - } - - var hasCoupon = customer.Discount?.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone; - - if (!hasCoupon) - { - return false; - } - - var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId); - - var couponAppliesTo = customer.Discount?.Coupon?.AppliesTo?.Products; - - return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); - } -} diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 494a202c4b..36a618f799 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -74,12 +74,16 @@ public class OrganizationBillingService( return OrganizationMetadata.Default; } - var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); + var isManaged = organization.Status == OrganizationStatusType.Managed; + var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { return OrganizationMetadata.Default with { + IsEligibleForSelfHost = isEligibleForSelfHost, + IsManaged = isManaged, OrganizationOccupiedSeats = orgOccupiedSeats.Total }; } @@ -93,14 +97,28 @@ public class OrganizationBillingService( { return OrganizationMetadata.Default with { - OrganizationOccupiedSeats = orgOccupiedSeats.Total + IsEligibleForSelfHost = isEligibleForSelfHost, + IsManaged = isManaged }; } var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); + var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId) + ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) + : null; + return new OrganizationMetadata( + isEligibleForSelfHost, + isManaged, isOnSecretsManagerStandalone, + subscription.Status == StripeConstants.SubscriptionStatus.Unpaid, + true, + invoice?.Status == StripeConstants.InvoiceStatus.Open, + subscription.Status == StripeConstants.SubscriptionStatus.Canceled, + invoice?.DueDate, + invoice?.Created, + subscription.CurrentPeriodEnd, orgOccupiedSeats.Total); } @@ -518,6 +536,16 @@ public class OrganizationBillingService( return customer; } + private async Task IsEligibleForSelfHostAsync( + Organization organization) + { + var plans = await pricingClient.ListPlans(); + + var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); + + return eligibleSelfHostPlans.Contains(organization.PlanType); + } + private async Task IsOnSecretsManagerStandalone( Organization organization, Customer? customer, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a3c637ecb1..80b74877c5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -179,7 +179,6 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; - public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure"; public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index d79bfde893..51866320ee 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -1,4 +1,5 @@ using Bit.Api.Billing.Controllers; +using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; @@ -52,16 +53,19 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, 10)); + .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); - Assert.IsType>(result); + Assert.IsType>(result); - var response = ((Ok)result).Value; + var response = ((Ok)result).Value; + Assert.True(response.IsEligibleForSelfHost); + Assert.True(response.IsManaged); Assert.True(response.IsOnSecretsManagerStandalone); - Assert.Equal(10, response.OrganizationOccupiedSeats); + Assert.True(response.IsSubscriptionUnpaid); + Assert.True(response.HasSubscription); } [Theory, BitAutoData] diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs deleted file mode 100644 index 21081112d7..0000000000 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs +++ /dev/null @@ -1,369 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Organizations.Models; -using Bit.Core.Billing.Organizations.Queries; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Repositories; -using Bit.Core.Settings; -using Bit.Core.Utilities; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using NSubstitute.ReturnsExtensions; -using Stripe; -using Xunit; - -namespace Bit.Core.Test.Billing.Organizations.Queries; - -[SutProviderCustomize] -public class GetOrganizationMetadataQueryTests -{ - [Theory, BitAutoData] - public async Task Run_NullOrganization_ReturnsNull( - SutProvider sutProvider) - { - var result = await sutProvider.Sut.Run(null); - - Assert.Null(result); - } - - [Theory, BitAutoData] - public async Task Run_SelfHosted_ReturnsDefault( - Organization organization, - SutProvider sutProvider) - { - sutProvider.GetDependency().SelfHosted.Returns(true); - - var result = await sutProvider.Sut.Run(organization); - - Assert.Equal(OrganizationMetadata.Default, result); - } - - [Theory, BitAutoData] - public async Task Run_NoGatewaySubscriptionId_ReturnsDefaultWithOccupiedSeats( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = null; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 10, Sponsored = 0 }); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(10, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_NullCustomer_ReturnsDefaultWithOccupiedSeats( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .ReturnsNull(); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(5, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_NullSubscription_ReturnsDefaultWithOccupiedSeats( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - - var customer = new Customer(); - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .Returns(customer); - - sutProvider.GetDependency() - .GetSubscription(organization) - .ReturnsNull(); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(7, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_WithSecretsManagerStandaloneCoupon_ReturnsMetadataWithFlag( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - organization.PlanType = PlanType.EnterpriseAnnually; - - var productId = "product_123"; - var customer = new Customer - { - Discount = new Discount - { - Coupon = new Coupon - { - Id = StripeConstants.CouponIDs.SecretsManagerStandalone, - AppliesTo = new CouponAppliesTo - { - Products = [productId] - } - } - } - }; - - var subscription = new Subscription - { - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Plan = new Plan - { - ProductId = productId - } - } - ] - } - }; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .Returns(customer); - - sutProvider.GetDependency() - .GetSubscription(organization) - .Returns(subscription); - - sutProvider.GetDependency() - .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.True(result.IsOnSecretsManagerStandalone); - Assert.Equal(15, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_WithoutSecretsManagerStandaloneCoupon_ReturnsMetadataWithoutFlag( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - organization.PlanType = PlanType.TeamsAnnually; - - var customer = new Customer - { - Discount = null - }; - - var subscription = new Subscription - { - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Plan = new Plan - { - ProductId = "product_123" - } - } - ] - } - }; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .Returns(customer); - - sutProvider.GetDependency() - .GetSubscription(organization) - .Returns(subscription); - - sutProvider.GetDependency() - .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(20, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_CouponDoesNotApplyToSubscriptionProducts_ReturnsFalseForStandaloneFlag( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - organization.PlanType = PlanType.EnterpriseAnnually; - - var customer = new Customer - { - Discount = new Discount - { - Coupon = new Coupon - { - Id = StripeConstants.CouponIDs.SecretsManagerStandalone, - AppliesTo = new CouponAppliesTo - { - Products = ["different_product_id"] - } - } - } - }; - - var subscription = new Subscription - { - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Plan = new Plan - { - ProductId = "product_123" - } - } - ] - } - }; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .Returns(customer); - - sutProvider.GetDependency() - .GetSubscription(organization) - .Returns(subscription); - - sutProvider.GetDependency() - .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(12, result.OrganizationOccupiedSeats); - } - - [Theory, BitAutoData] - public async Task Run_PlanDoesNotSupportSecretsManager_ReturnsFalseForStandaloneFlag( - Organization organization, - SutProvider sutProvider) - { - organization.GatewaySubscriptionId = "sub_123"; - organization.PlanType = PlanType.FamiliesAnnually; - - var productId = "product_123"; - var customer = new Customer - { - Discount = new Discount - { - Coupon = new Coupon - { - Id = StripeConstants.CouponIDs.SecretsManagerStandalone, - AppliesTo = new CouponAppliesTo - { - Products = [productId] - } - } - } - }; - - var subscription = new Subscription - { - Items = new StripeList - { - Data = - [ - new SubscriptionItem - { - Plan = new Plan - { - ProductId = productId - } - } - ] - } - }; - - sutProvider.GetDependency().SelfHosted.Returns(false); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 }); - - sutProvider.GetDependency() - .GetCustomer(organization, Arg.Is(options => - options.Expand.Contains("discount.coupon.applies_to"))) - .Returns(customer); - - sutProvider.GetDependency() - .GetSubscription(organization) - .Returns(subscription); - - sutProvider.GetDependency() - .GetPlanOrThrow(organization.PlanType) - .Returns(StaticStore.GetPlan(organization.PlanType)); - - var result = await sutProvider.Sut.Run(organization); - - Assert.NotNull(result); - Assert.False(result.IsOnSecretsManagerStandalone); - Assert.Equal(8, result.OrganizationOccupiedSeats); - } -} diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 77dce8101c..7edc60a26a 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -96,10 +96,6 @@ public class OrganizationBillingServiceTests sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 }); - var subscriberService = sutProvider.GetDependency(); // Set up subscriber service to return null for customer @@ -114,7 +110,13 @@ public class OrganizationBillingServiceTests Assert.NotNull(metadata); Assert.False(metadata!.IsOnSecretsManagerStandalone); - Assert.Equal(1, metadata.OrganizationOccupiedSeats); + Assert.False(metadata.HasSubscription); + Assert.False(metadata.IsSubscriptionUnpaid); + Assert.False(metadata.HasOpenInvoice); + Assert.False(metadata.IsSubscriptionCanceled); + Assert.Null(metadata.InvoiceDueDate); + Assert.Null(metadata.InvoiceCreatedDate); + Assert.Null(metadata.SubPeriodEndDate); } #endregion From a565fd9ee4b2d3b86679c270222889ecc0944b29 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:39:31 -0400 Subject: [PATCH 10/52] 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 --- dev/servicebusemulator_config.json | 33 +- .../Controllers/SlackIntegrationController.cs | 6 +- .../Controllers/TeamsIntegrationController.cs | 147 +++++++ ...ionIntegrationConfigurationRequestModel.cs | 4 + .../OrgnizationIntegrationRequestModel.cs | 2 +- .../OrganizationIntegrationResponseModel.cs | 14 +- src/Api/Startup.cs | 3 +- .../AdminConsole/Enums/IntegrationType.cs | 5 +- .../EventIntegrations/TeamsIntegration.cs | 12 + .../TeamsIntegrationConfigurationDetails.cs | 3 + .../TeamsListenerConfiguration.cs | 38 ++ .../Models/Slack/SlackApiResponse.cs | 4 +- .../Models/Teams/TeamsApiResponse.cs | 41 ++ .../Teams/TeamsBotCredentialProvider.cs | 28 ++ .../IOrganizationIntegrationRepository.cs | 2 + .../AdminConsole/Services/ISlackService.cs | 48 +++ .../AdminConsole/Services/ITeamsService.cs | 49 +++ .../EventIntegrations/README.md | 53 ++- .../EventIntegrations/SlackService.cs | 6 + .../TeamsIntegrationHandler.cs | 41 ++ .../EventIntegrations/TeamsService.cs | 182 ++++++++ .../NoopImplementations/NoopTeamsService.cs | 27 ++ src/Core/Core.csproj | 2 + src/Core/Settings/GlobalSettings.cs | 15 + .../OrganizationIntegrationRepository.cs | 13 + .../OrganizationIntegrationRepository.cs | 12 + ...ByTeamsConfigurationTenantIdTeamIdQuery.cs | 36 ++ src/SharedWeb/SharedWeb.csproj | 1 + .../Utilities/ServiceCollectionExtensions.cs | 35 ++ ...ReadByTeamsConfigurationTenantIdTeamId.sql | 17 + .../SlackIntegrationControllerTests.cs | 30 +- .../TeamsIntegrationControllerTests.cs | 392 ++++++++++++++++++ ...tegrationConfigurationRequestModelTests.cs | 36 +- ...rganizationIntegrationRequestModelTests.cs | 16 + ...ganizationIntegrationResponseModelTests.cs | 43 ++ .../Teams/TeamsBotCredentialProviderTests.cs | 56 +++ .../Services/IntegrationTypeTests.cs | 41 +- .../Services/SlackServiceTests.cs | 12 + .../Services/TeamsIntegrationHandlerTests.cs | 126 ++++++ .../Services/TeamsServiceTests.cs | 289 +++++++++++++ ...igurationTenantIdTeamIdStoredProcedure.sql | 18 + 41 files changed, 1839 insertions(+), 99 deletions(-) create mode 100644 src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs create mode 100644 src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs create mode 100644 src/Core/AdminConsole/Services/ITeamsService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs create mode 100644 src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId.sql create mode 100644 test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs create mode 100644 test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs create mode 100644 util/Migrator/DbScripts/2025-10-3_00_AddOrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamIdStoredProcedure.sql diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index 294efc1897..bb50c0b1ee 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -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", @@ -37,6 +21,9 @@ }, { "Name": "events-datadog-subscription" + }, + { + "Name": "events-teams-subscription" } ] }, @@ -98,6 +85,20 @@ } } ] + }, + { + "Name": "integration-teams-subscription", + "Rules": [ + { + "Name": "teams-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "teams" + } + } + } + ] } ] } diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs index c8ff4f9f7c..08635878de 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs @@ -32,7 +32,7 @@ public class SlackIntegrationController( } string? callbackUrl = Url.RouteUrl( - routeName: nameof(CreateAsync), + routeName: "SlackIntegration_Create", values: null, protocol: currentContext.HttpContext.Request.Scheme, host: currentContext.HttpContext.Request.Host.ToUriComponent() @@ -76,7 +76,7 @@ public class SlackIntegrationController( return Redirect(redirectUrl); } - [HttpGet("integrations/slack/create", Name = nameof(CreateAsync))] + [HttpGet("integrations/slack/create", Name = "SlackIntegration_Create")] [AllowAnonymous] public async Task CreateAsync([FromQuery] string code, [FromQuery] string state) { @@ -103,7 +103,7 @@ public class SlackIntegrationController( // Fetch token from Slack and store to DB string? callbackUrl = Url.RouteUrl( - routeName: nameof(CreateAsync), + routeName: "SlackIntegration_Create", values: null, protocol: currentContext.HttpContext.Request.Scheme, host: currentContext.HttpContext.Request.Host.ToUriComponent() diff --git a/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs b/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs new file mode 100644 index 0000000000..8cafb6b2cf --- /dev/null +++ b/src/Api/AdminConsole/Controllers/TeamsIntegrationController.cs @@ -0,0 +1,147 @@ +using System.Text.Json; +using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace Bit.Api.AdminConsole.Controllers; + +[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)] +[Route("organizations")] +[Authorize("Application")] +public class TeamsIntegrationController( + ICurrentContext currentContext, + IOrganizationIntegrationRepository integrationRepository, + IBot bot, + IBotFrameworkHttpAdapter adapter, + ITeamsService teamsService, + TimeProvider timeProvider) : Controller +{ + [HttpGet("{organizationId:guid}/integrations/teams/redirect")] + public async Task RedirectAsync(Guid organizationId) + { + if (!await currentContext.OrganizationOwner(organizationId)) + { + throw new NotFoundException(); + } + + var callbackUrl = Url.RouteUrl( + routeName: "TeamsIntegration_Create", + values: null, + protocol: currentContext.HttpContext.Request.Scheme, + host: currentContext.HttpContext.Request.Host.ToUriComponent() + ); + if (string.IsNullOrEmpty(callbackUrl)) + { + throw new BadRequestException("Unable to build callback Url"); + } + + var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId); + var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Teams); + + if (integration is null) + { + // No teams integration exists, create Initiated version + integration = await integrationRepository.CreateAsync(new OrganizationIntegration + { + OrganizationId = organizationId, + Type = IntegrationType.Teams, + Configuration = null, + }); + } + else if (integration.Configuration is not null) + { + // A Completed (fully configured) Teams integration already exists, throw to prevent overriding + throw new BadRequestException("There already exists a Teams integration for this organization"); + + } // An Initiated teams integration exits, re-use it and kick off a new OAuth flow + + var state = IntegrationOAuthState.FromIntegration(integration, timeProvider); + var redirectUrl = teamsService.GetRedirectUrl( + callbackUrl: callbackUrl, + state: state.ToString() + ); + + if (string.IsNullOrEmpty(redirectUrl)) + { + throw new NotFoundException(); + } + + return Redirect(redirectUrl); + } + + [HttpGet("integrations/teams/create", Name = "TeamsIntegration_Create")] + [AllowAnonymous] + public async Task CreateAsync([FromQuery] string code, [FromQuery] string state) + { + var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider); + if (oAuthState is null) + { + throw new NotFoundException(); + } + + // Fetch existing Initiated record + var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId); + if (integration is null || + integration.Type != IntegrationType.Teams || + integration.Configuration is not null) + { + throw new NotFoundException(); + } + + // Verify Organization matches hash + if (!oAuthState.ValidateOrg(integration.OrganizationId)) + { + throw new NotFoundException(); + } + + var callbackUrl = Url.RouteUrl( + routeName: "TeamsIntegration_Create", + values: null, + protocol: currentContext.HttpContext.Request.Scheme, + host: currentContext.HttpContext.Request.Host.ToUriComponent() + ); + if (string.IsNullOrEmpty(callbackUrl)) + { + throw new BadRequestException("Unable to build callback Url"); + } + + var token = await teamsService.ObtainTokenViaOAuth(code, callbackUrl); + if (string.IsNullOrEmpty(token)) + { + throw new BadRequestException("Invalid response from Teams."); + } + + var teams = await teamsService.GetJoinedTeamsAsync(token); + + if (!teams.Any()) + { + throw new BadRequestException("No teams were found."); + } + + var teamsIntegration = new TeamsIntegration(TenantId: teams[0].TenantId, Teams: teams); + integration.Configuration = JsonSerializer.Serialize(teamsIntegration); + await integrationRepository.UpsertAsync(integration); + + var location = $"/organizations/{integration.OrganizationId}/integrations/{integration.Id}"; + return Created(location, new OrganizationIntegrationResponseModel(integration)); + } + + [Route("integrations/teams/incoming")] + [AllowAnonymous] + [HttpPost] + public async Task IncomingPostAsync() + { + await adapter.ProcessAsync(Request, Response, bot); + } +} diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index 7d1efe2315..8581c4ae1f 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -38,6 +38,10 @@ public class OrganizationIntegrationConfigurationRequestModel return !string.IsNullOrWhiteSpace(Template) && Configuration is null && IsFiltersValid(); + case IntegrationType.Teams: + return !string.IsNullOrWhiteSpace(Template) && + Configuration is null && + IsFiltersValid(); default: return false; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs index 92d65ab8fe..668afe70bf 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs @@ -35,7 +35,7 @@ public class OrganizationIntegrationRequestModel : IValidatableObject case IntegrationType.CloudBillingSync or IntegrationType.Scim: yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", [nameof(Type)]); break; - case IntegrationType.Slack: + case IntegrationType.Slack or IntegrationType.Teams: yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", [nameof(Type)]); break; case IntegrationType.Webhook: diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs index 5368f78e39..0c31e07bef 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Models.Api; @@ -35,6 +37,16 @@ public class OrganizationIntegrationResponseModel : ResponseModel ? OrganizationIntegrationStatus.Initiated : OrganizationIntegrationStatus.Completed, + // If present and the configuration is null, OAuth has been initiated, and we are + // waiting on the return OAuth call. If Configuration is not null and IsCompleted is true, + // then we've received the app install bot callback, and it's Completed. Otherwise, + // it is In Progress while we await the app install bot callback. + IntegrationType.Teams => string.IsNullOrWhiteSpace(Configuration) + ? OrganizationIntegrationStatus.Initiated + : (JsonSerializer.Deserialize(Configuration)?.IsCompleted ?? false) + ? OrganizationIntegrationStatus.Completed + : OrganizationIntegrationStatus.InProgress, + // HEC and Datadog should only be allowed to be created non-null. // If they are null, they are Invalid IntegrationType.Hec => string.IsNullOrWhiteSpace(Configuration) diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index cc50a1b362..5d9918d1d4 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -229,8 +229,9 @@ public class Startup services.AddHostedService(); } - // Add SlackService for OAuth API requests - if configured + // Add Slack / Teams Services for OAuth API requests - if configured services.AddSlackService(globalSettings); + services.AddTeamsService(globalSettings); } public void Configure( diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs index 34edc71fbe..84e4de94e9 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -7,7 +7,8 @@ public enum IntegrationType : int Slack = 3, Webhook = 4, Hec = 5, - Datadog = 6 + Datadog = 6, + Teams = 7 } public static class IntegrationTypeExtensions @@ -24,6 +25,8 @@ public static class IntegrationTypeExtensions return "hec"; case IntegrationType.Datadog: return "datadog"; + case IntegrationType.Teams: + return "teams"; default: throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}"); } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs new file mode 100644 index 0000000000..8390022839 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegration.cs @@ -0,0 +1,12 @@ +using Bit.Core.Models.Teams; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record TeamsIntegration( + string TenantId, + IReadOnlyList Teams, + string? ChannelId = null, + Uri? ServiceUrl = null) +{ + public bool IsCompleted => !string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null; +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs new file mode 100644 index 0000000000..66fe558dff --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsIntegrationConfigurationDetails.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record TeamsIntegrationConfigurationDetails(string ChannelId, Uri ServiceUrl); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs new file mode 100644 index 0000000000..24cf674648 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/TeamsListenerConfiguration.cs @@ -0,0 +1,38 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class TeamsListenerConfiguration(GlobalSettings globalSettings) : + ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration +{ + public IntegrationType IntegrationType + { + get => IntegrationType.Teams; + } + + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.TeamsEventsQueueName; + } + + public string IntegrationQueueName + { + get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationQueueName; + } + + public string IntegrationRetryQueueName + { + get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationRetryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.TeamsEventSubscriptionName; + } + + public string IntegrationSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.TeamsIntegrationSubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs index ede2123f7e..70d280c428 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Bit.Core.Models.Slack; diff --git a/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs b/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs new file mode 100644 index 0000000000..131e45264f --- /dev/null +++ b/src/Core/AdminConsole/Models/Teams/TeamsApiResponse.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace Bit.Core.Models.Teams; + +/// Represents the response returned by the Microsoft OAuth 2.0 token endpoint. +/// See Microsoft identity platform and OAuth 2.0 +/// authorization code flow. +public class TeamsOAuthResponse +{ + /// The access token issued by Microsoft, used to call the Microsoft Graph API. + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } = string.Empty; +} + +/// Represents the response from the /me/joinedTeams Microsoft Graph API call. +/// See List joined teams - +/// Microsoft Graph v1.0. +public class JoinedTeamsResponse +{ + /// The collection of teams that the user has joined. + [JsonPropertyName("value")] + public List Value { get; set; } = []; +} + +/// Represents a Microsoft Teams team returned by the Graph API. +/// See Team resource type - +/// Microsoft Graph v1.0. +public class TeamInfo +{ + /// The unique identifier of the team. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// The name of the team. + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// The ID of the Microsoft Entra tenant for this team. + [JsonPropertyName("tenantId")] + public string TenantId { get; set; } = string.Empty; +} diff --git a/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs b/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs new file mode 100644 index 0000000000..eeb17131a3 --- /dev/null +++ b/src/Core/AdminConsole/Models/Teams/TeamsBotCredentialProvider.cs @@ -0,0 +1,28 @@ +using Microsoft.Bot.Connector.Authentication; + +namespace Bit.Core.AdminConsole.Models.Teams; + +public class TeamsBotCredentialProvider(string clientId, string clientSecret) : ICredentialProvider +{ + private const string _microsoftBotFrameworkIssuer = AuthenticationConstants.ToBotFromChannelTokenIssuer; + + public Task IsValidAppIdAsync(string appId) + { + return Task.FromResult(appId == clientId); + } + + public Task GetAppPasswordAsync(string appId) + { + return Task.FromResult(appId == clientId ? clientSecret : null); + } + + public Task IsAuthenticationDisabledAsync() + { + return Task.FromResult(false); + } + + public Task ValidateIssuerAsync(string issuer) + { + return Task.FromResult(issuer == _microsoftBotFrameworkIssuer); + } +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs index 434c8ddee3..1d8b8be0ec 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs @@ -5,4 +5,6 @@ namespace Bit.Core.Repositories; public interface IOrganizationIntegrationRepository : IRepository { Task> GetManyByOrganizationAsync(Guid organizationId); + + Task GetByTeamsConfigurationTenantIdTeamId(string tenantId, string teamId); } diff --git a/src/Core/AdminConsole/Services/ISlackService.cs b/src/Core/AdminConsole/Services/ISlackService.cs index ff1e03f051..0577532ac2 100644 --- a/src/Core/AdminConsole/Services/ISlackService.cs +++ b/src/Core/AdminConsole/Services/ISlackService.cs @@ -1,11 +1,59 @@ namespace Bit.Core.Services; +/// Defines operations for interacting with Slack, including OAuth authentication, channel discovery, +/// and sending messages. public interface ISlackService { + /// Note: This API is not currently used (yet) by any server code. It is here to provide functionality if + /// the UI needs to be able to look up channels for a user. + /// Retrieves the ID of a Slack channel by name. + /// See conversations.list API. + /// A valid Slack OAuth access token. + /// The name of the channel to look up. + /// The channel ID if found; otherwise, an empty string. Task GetChannelIdAsync(string token, string channelName); + + /// Note: This API is not currently used (yet) by any server code. It is here to provide functionality if + /// the UI needs to be able to look up channels for a user. + /// Retrieves the IDs of multiple Slack channels by name. + /// See conversations.list API. + /// A valid Slack OAuth access token. + /// A list of channel names to look up. + /// A list of matching channel IDs. Channels that cannot be found are omitted. Task> GetChannelIdsAsync(string token, List channelNames); + + /// Note: This API is not currently used (yet) by any server code. It is here to provide functionality if + /// the UI needs to be able to look up a user by their email address. + /// Retrieves the DM channel ID for a Slack user by email. + /// See users.lookupByEmail API and + /// conversations.open API. + /// A valid Slack OAuth access token. + /// The email address of the user to open a DM with. + /// The DM channel ID if successful; otherwise, an empty string. Task GetDmChannelByEmailAsync(string token, string email); + + /// Builds the Slack OAuth 2.0 authorization URL for the app. + /// See Slack OAuth v2 documentation. + /// The absolute redirect URI that Slack will call after user authorization. + /// Must match the URI registered with the app configuration. + /// A state token used to correlate the request and callback and prevent CSRF attacks. + /// The full authorization URL to which the user should be redirected to begin the sign-in process. string GetRedirectUrl(string callbackUrl, string state); + + /// Exchanges a Slack OAuth code for an access token. + /// See oauth.v2.access API. + /// The authorization code returned by Slack via the callback URL after user authorization. + /// The redirect URI that was used in the authorization request. + /// A valid Slack access token if successful; otherwise, an empty string. Task ObtainTokenViaOAuth(string code, string redirectUrl); + + /// Sends a message to a Slack channel by ID. + /// See chat.postMessage API. + /// This is used primarily by the to send events to the + /// Slack channel. + /// A valid Slack OAuth access token. + /// The message text to send. + /// The channel ID to send the message to. + /// A task that completes when the message has been sent. Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId); } diff --git a/src/Core/AdminConsole/Services/ITeamsService.cs b/src/Core/AdminConsole/Services/ITeamsService.cs new file mode 100644 index 0000000000..e3757987c3 --- /dev/null +++ b/src/Core/AdminConsole/Services/ITeamsService.cs @@ -0,0 +1,49 @@ +using Bit.Core.Models.Teams; + +namespace Bit.Core.Services; + +/// +/// Service that provides functionality relating to the Microsoft Teams integration including OAuth, +/// team discovery and sending a message to a channel in Teams. +/// +public interface ITeamsService +{ + /// + /// Generate the Microsoft Teams OAuth 2.0 authorization URL used to begin the sign-in flow. + /// + /// The absolute redirect URI that Microsoft will call after user authorization. + /// Must match the URI registered with the app configuration. + /// A state token used to correlate the request and callback and prevent CSRF attacks. + /// The full authorization URL to which the user should be redirected to begin the sign-in process. + string GetRedirectUrl(string callbackUrl, string state); + + /// + /// Exchange the OAuth code for a Microsoft Graph API access token. + /// + /// The code returned from Microsoft via the OAuth callback Url. + /// The same redirect URI that was passed to the authorization request. + /// A valid Microsoft Graph access token if the exchange succeeds; otherwise, an empty string. + Task ObtainTokenViaOAuth(string code, string redirectUrl); + + /// + /// Get the Teams to which the authenticated user belongs via Microsoft Graph API. + /// + /// A valid Microsoft Graph access token for the user (obtained via OAuth). + /// A read-only list of objects representing the user’s joined teams. + /// Returns an empty list if the request fails or if the token is invalid. + Task> GetJoinedTeamsAsync(string accessToken); + + /// + /// Send a message to a specific channel in Teams. + /// + /// This is used primarily by the to send events to the + /// Teams channel. + /// The service URI associated with the Microsoft Bot Framework connector for the target + /// team. Obtained via the bot framework callback. + /// The conversation or channel ID where the message should be delivered. Obtained via + /// the bot framework callback. + /// The message text to post to the channel. + /// A task that completes when the message has been sent. Errors during message delivery are surfaced + /// as exceptions from the underlying connector client. + Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index de7ce3f7fd..7570d47211 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -203,31 +203,17 @@ Currently, there are integrations / handlers for Slack, webhooks, and HTTP Event - The top-level object that enables a specific integration for the organization. - Includes any properties that apply to the entire integration across all events. - - For Slack, it consists of the token: `{ "Token": "xoxb-token-from-slack" }`. - - For webhooks, it is optional. Webhooks can either be configured at this level or the configuration level, - but the configuration level takes precedence. However, even though it is optional, an organization must - have a webhook `OrganizationIntegration` (even will a `null` `Configuration`) to enable configuration - via `OrganizationIntegrationConfiguration`. - - For HEC, it consists of the scheme, token, and URI: - -```json - { - "Scheme": "Bearer", - "Token": "Auth-token-from-HEC-service", - "Uri": "https://example.com/api" - } -``` + - For example, Slack stores the token in the `Configuration` which applies to every event, but stores the +channel id in the `Configuration` of the `OrganizationIntegrationConfiguration`. The token applies to the entire Slack +integration, but the channel could be configured differently depending on event type. + - See the table below for more examples / details on what is stored at which level. ### `OrganizationIntegrationConfiguration` - This contains the configurations specific to each `EventType` for the integration. - `Configuration` contains the event-specific configuration. - - For Slack, this would contain what channel to send the message to: `{ "channelId": "C123456" }` - - For webhooks, this is the URL the request should be sent to: `{ "url": "https://api.example.com" }` - - Optionally this also can include a `Scheme` and `Token` if this webhook needs Authentication. - - As stated above, all of this information can be specified here or at the `OrganizationIntegration` - level, but any properties declared here will take precedence over the ones above. - - For HEC, this must be null. HEC is configured only at the `OrganizationIntegration` level. + - Any properties at this level override the `Configuration` form the `OrganizationIntegration`. + - See the table below for examples of specific integrations. - `Template` contains a template string that is expected to be filled in with the contents of the actual event. - The tokens in the string are wrapped in `#` characters. For instance, the UserId would be `#UserId#`. - The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with introspected values from @@ -245,6 +231,23 @@ Currently, there are integrations / handlers for Slack, webhooks, and HTTP Event - An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from the database to determine what to publish at the integration level. +### Existing integrations and the configurations at each level + +The following table illustrates how each integration is configured and what exactly is stored in the `Configuration` +property at each level (`OrganizationIntegration` or `OrganizationIntegrationConfiguration`). Under +`OrganizationIntegration` the valid `OrganizationIntegrationStatus` are in bold, with an example of what would be +stored at each status. + +| **Integration** | **OrganizationIntegration** | **OrganizationIntegrationConfiguration** | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| CloudBillingSync | **Not Applicable** (not yet used) | **Not Applicable** (not yet used) | +| Scim | **Not Applicable** (not yet used) | **Not Applicable** (not yet used) | +| Slack | **Initiated**: `null`
**Completed**:
`{ "Token": "xoxb-token-from-slack" }` | `{ "channelId": "C123456" }` | +| Webhook | `null` or `{ "Scheme": "Bearer", "Token": "AUTH-TOKEN", "Uri": "https://example.com" }` | `null` or `{ "Scheme": "Bearer", "Token":"AUTH-TOKEN", "Uri": "https://example.com" }`

Whatever is defined at this level takes precedence | +| Hec | `{ "Scheme": "Bearer", "Token": "AUTH-TOKEN", "Uri": "https://example.com" }` | Always `null` | +| Datadog | `{ "ApiKey": "TheKey12345", "Uri": "https://api.us5.datadoghq.com/api/v1/events"}` | Always `null` | +| Teams | **Initiated**: `null`
**In Progress**:
`{ "TenantID": "tenant", "Teams": ["Id": "team", DisplayName: "MyTeam"]}`
**Completed**:
`{ "TenantID": "tenant", "Teams": ["Id": "team", DisplayName: "MyTeam"], "ServiceUrl":"https://example.com", ChannelId: "channel-1234"}` | Always `null` | + ## Filtering In addition to the ability to configure integrations mentioned above, organization admins can @@ -349,10 +352,20 @@ and event type. - This will be the deserialized version of the `MergedConfiguration` in `OrganizationIntegrationConfigurationDetails`. +A new row with the new integration should be added to this doc in the table above [Existing integrations +and the configurations at each level](#existing-integrations-and-the-configurations-at-each-level). + ## Request Models 1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate`. + - Additionally, add tests in `OrganizationIntegrationRequestModelTests` 2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType`. + - Additionally, add / update tests in `OrganizationIntegrationConfigurationRequestModelTests` + +## Response Model + +1. Add a new case to the switch method in `OrganizationIntegrationResponseModel.Status`. + - Additionally, add / update tests in `OrganizationIntegrationResponseModelTests` ## Integration Handler diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs index 4fb74f1f44..8b691dd4bf 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs @@ -90,6 +90,12 @@ public class SlackService( public async Task ObtainTokenViaOAuth(string code, string redirectUrl) { + if (string.IsNullOrEmpty(code) || string.IsNullOrWhiteSpace(redirectUrl)) + { + logger.LogError("Error obtaining token via OAuth: Code and/or RedirectUrl were empty"); + return string.Empty; + } + var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access", new FormUrlEncodedContent(new[] { diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs new file mode 100644 index 0000000000..41d60bd69c --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsIntegrationHandler.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Microsoft.Rest; + +namespace Bit.Core.Services; + +public class TeamsIntegrationHandler( + ITeamsService teamsService) + : IntegrationHandlerBase +{ + public override async Task HandleAsync( + IntegrationMessage message) + { + try + { + await teamsService.SendMessageToChannelAsync( + serviceUri: message.Configuration.ServiceUrl, + message: message.RenderedTemplate, + channelId: message.Configuration.ChannelId + ); + + return new IntegrationHandlerResult(success: true, message: message); + } + catch (HttpOperationException ex) + { + var result = new IntegrationHandlerResult(success: false, message: message); + var statusCode = (int)ex.Response.StatusCode; + result.Retryable = statusCode is 429 or >= 500 and < 600; + result.FailureReason = ex.Message; + + return result; + } + catch (Exception ex) + { + var result = new IntegrationHandlerResult(success: false, message: message); + result.Retryable = false; + result.FailureReason = ex.Message; + + return result; + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs new file mode 100644 index 0000000000..f9911760bb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/TeamsService.cs @@ -0,0 +1,182 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Web; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Models.Teams; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Teams; +using Microsoft.Bot.Connector; +using Microsoft.Bot.Connector.Authentication; +using Microsoft.Bot.Schema; +using Microsoft.Extensions.Logging; +using TeamInfo = Bit.Core.Models.Teams.TeamInfo; + +namespace Bit.Core.Services; + +public class TeamsService( + IHttpClientFactory httpClientFactory, + IOrganizationIntegrationRepository integrationRepository, + GlobalSettings globalSettings, + ILogger logger) : ActivityHandler, ITeamsService +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + private readonly string _clientId = globalSettings.Teams.ClientId; + private readonly string _clientSecret = globalSettings.Teams.ClientSecret; + private readonly string _scopes = globalSettings.Teams.Scopes; + private readonly string _graphBaseUrl = globalSettings.Teams.GraphBaseUrl; + private readonly string _loginBaseUrl = globalSettings.Teams.LoginBaseUrl; + + public const string HttpClientName = "TeamsServiceHttpClient"; + + public string GetRedirectUrl(string redirectUrl, string state) + { + var query = HttpUtility.ParseQueryString(string.Empty); + query["client_id"] = _clientId; + query["response_type"] = "code"; + query["redirect_uri"] = redirectUrl; + query["response_mode"] = "query"; + query["scope"] = string.Join(" ", _scopes); + query["state"] = state; + + return $"{_loginBaseUrl}/common/oauth2/v2.0/authorize?{query}"; + } + + public async Task ObtainTokenViaOAuth(string code, string redirectUrl) + { + if (string.IsNullOrEmpty(code) || string.IsNullOrWhiteSpace(redirectUrl)) + { + logger.LogError("Error obtaining token via OAuth: Code and/or RedirectUrl were empty"); + return string.Empty; + } + + var request = new HttpRequestMessage(HttpMethod.Post, + $"{_loginBaseUrl}/common/oauth2/v2.0/token"); + + request.Content = new FormUrlEncodedContent(new Dictionary + { + { "client_id", _clientId }, + { "client_secret", _clientSecret }, + { "code", code }, + { "redirect_uri", redirectUrl }, + { "grant_type", "authorization_code" } + }); + + using var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + var errorText = await response.Content.ReadAsStringAsync(); + logger.LogError("Teams OAuth token exchange failed: {errorText}", errorText); + return string.Empty; + } + + TeamsOAuthResponse? result; + try + { + result = await response.Content.ReadFromJsonAsync(); + } + catch + { + result = null; + } + + if (result is null) + { + logger.LogError("Error obtaining token via OAuth: Unknown error"); + return string.Empty; + } + + return result.AccessToken; + } + + public async Task> GetJoinedTeamsAsync(string accessToken) + { + using var request = new HttpRequestMessage( + HttpMethod.Get, + $"{_graphBaseUrl}/me/joinedTeams"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + var errorText = await response.Content.ReadAsStringAsync(); + logger.LogError("Get Teams request failed: {errorText}", errorText); + return new List(); + } + + var result = await response.Content.ReadFromJsonAsync(); + + return result?.Value ?? []; + } + + public async Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message) + { + var credentials = new MicrosoftAppCredentials(_clientId, _clientSecret); + using var connectorClient = new ConnectorClient(serviceUri, credentials); + + var activity = new Activity + { + Type = ActivityTypes.Message, + Text = message + }; + + await connectorClient.Conversations.SendToConversationAsync(channelId, activity); + } + + protected override async Task OnInstallationUpdateAddAsync(ITurnContext turnContext, + CancellationToken cancellationToken) + { + var conversationId = turnContext.Activity.Conversation.Id; + var serviceUrl = turnContext.Activity.ServiceUrl; + var teamId = turnContext.Activity.TeamsGetTeamInfo().AadGroupId; + var tenantId = turnContext.Activity.Conversation.TenantId; + + if (!string.IsNullOrWhiteSpace(conversationId) && + !string.IsNullOrWhiteSpace(serviceUrl) && + Uri.TryCreate(serviceUrl, UriKind.Absolute, out var parsedUri) && + !string.IsNullOrWhiteSpace(teamId) && + !string.IsNullOrWhiteSpace(tenantId)) + { + await HandleIncomingAppInstallAsync( + conversationId: conversationId, + serviceUrl: parsedUri, + teamId: teamId, + tenantId: tenantId + ); + } + + await base.OnInstallationUpdateAddAsync(turnContext, cancellationToken); + } + + internal async Task HandleIncomingAppInstallAsync( + string conversationId, + Uri serviceUrl, + string teamId, + string tenantId) + { + var integration = await integrationRepository.GetByTeamsConfigurationTenantIdTeamId( + tenantId: tenantId, + teamId: teamId); + + if (integration?.Configuration is null) + { + return; + } + + var teamsConfig = JsonSerializer.Deserialize(integration.Configuration); + if (teamsConfig is null || teamsConfig.IsCompleted) + { + return; + } + + integration.Configuration = JsonSerializer.Serialize(teamsConfig with + { + ChannelId = conversationId, + ServiceUrl = serviceUrl + }); + + await integrationRepository.UpsertAsync(integration); + } +} diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs new file mode 100644 index 0000000000..fafb23f570 --- /dev/null +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopTeamsService.cs @@ -0,0 +1,27 @@ +using Bit.Core.Models.Teams; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.Services.NoopImplementations; + +public class NoopTeamsService : ITeamsService +{ + public string GetRedirectUrl(string callbackUrl, string state) + { + return string.Empty; + } + + public Task ObtainTokenViaOAuth(string code, string redirectUrl) + { + return Task.FromResult(string.Empty); + } + + public Task> GetJoinedTeamsAsync(string accessToken) + { + return Task.FromResult>(Array.Empty()); + } + + public Task SendMessageToChannelAsync(Uri serviceUri, string channelId, string message) + { + return Task.CompletedTask; + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index e9bf1b1807..23cb885bd4 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -38,6 +38,8 @@ + + diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 250daf0007..d79b7290ec 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -62,6 +62,7 @@ public class GlobalSettings : IGlobalSettings public virtual SqlSettings MySql { get; set; } = new SqlSettings(); public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" }; public virtual SlackSettings Slack { get; set; } = new SlackSettings(); + public virtual TeamsSettings Teams { get; set; } = new TeamsSettings(); public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); @@ -295,6 +296,15 @@ public class GlobalSettings : IGlobalSettings public virtual string Scopes { get; set; } } + public class TeamsSettings + { + public virtual string LoginBaseUrl { get; set; } = "https://login.microsoftonline.com"; + public virtual string GraphBaseUrl { get; set; } = "https://graph.microsoft.com/v1.0"; + public virtual string ClientId { get; set; } + public virtual string ClientSecret { get; set; } + public virtual string Scopes { get; set; } + } + public class EventLoggingSettings { public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); @@ -320,6 +330,8 @@ public class GlobalSettings : IGlobalSettings public virtual string HecIntegrationSubscriptionName { get; set; } = "integration-hec-subscription"; public virtual string DatadogEventSubscriptionName { get; set; } = "events-datadog-subscription"; public virtual string DatadogIntegrationSubscriptionName { get; set; } = "integration-datadog-subscription"; + public virtual string TeamsEventSubscriptionName { get; set; } = "events-teams-subscription"; + public virtual string TeamsIntegrationSubscriptionName { get; set; } = "integration-teams-subscription"; public string ConnectionString { @@ -364,6 +376,9 @@ public class GlobalSettings : IGlobalSettings public virtual string DatadogEventsQueueName { get; set; } = "events-datadog-queue"; public virtual string DatadogIntegrationQueueName { get; set; } = "integration-datadog-queue"; public virtual string DatadogIntegrationRetryQueueName { get; set; } = "integration-datadog-retry-queue"; + public virtual string TeamsEventsQueueName { get; set; } = "events-teams-queue"; + public virtual string TeamsIntegrationQueueName { get; set; } = "integration-teams-queue"; + public virtual string TeamsIntegrationRetryQueueName { get; set; } = "integration-teams-retry-queue"; public string HostName { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs index ece9697a31..4f8fb979d3 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -29,4 +29,17 @@ public class OrganizationIntegrationRepository : Repository GetByTeamsConfigurationTenantIdTeamId(string tenantId, string teamId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + "[dbo].[OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId]", + new { TenantId = tenantId, TeamId = teamId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs index 5670b2ae9b..c11591efcd 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -26,4 +26,16 @@ public class OrganizationIntegrationRepository : return await query.Run(dbContext).ToListAsync(); } } + + public async Task GetByTeamsConfigurationTenantIdTeamId( + string tenantId, + string teamId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery(tenantId: tenantId, teamId: teamId); + return await query.Run(dbContext).SingleOrDefaultAsync(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs new file mode 100644 index 0000000000..a1e86d9add --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery.cs @@ -0,0 +1,36 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; + +public class OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery : IQuery +{ + private readonly string _tenantId; + private readonly string _teamId; + + public OrganizationIntegrationReadByTeamsConfigurationTenantIdTeamIdQuery(string tenantId, string teamId) + { + _tenantId = tenantId; + _teamId = teamId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = + from oi in dbContext.OrganizationIntegrations + where oi.Type == IntegrationType.Teams && + oi.Configuration != null && + oi.Configuration.Contains($"\"TenantId\":\"{_tenantId}\"") && + oi.Configuration.Contains($"\"id\":\"{_teamId}\"") + select new OrganizationIntegration() + { + Id = oi.Id, + OrganizationId = oi.OrganizationId, + Type = oi.Type, + Configuration = oi.Configuration, + }; + return query; + } +} diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index 8bffa285fc..d8dc61178d 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,6 +7,7 @@ + diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 58ce0466c3..bc8df87599 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Bit.Core; using Bit.Core.AdminConsole.AbilitiesCache; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; @@ -69,6 +70,8 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Azure.Cosmos.Fluent; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Extensions.Caching.Cosmos; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; @@ -604,6 +607,33 @@ public static class ServiceCollectionExtensions return services; } + public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings) + { + if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) && + CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) && + CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes)) + { + services.AddHttpClient(TeamsService.HttpClientName); + services.TryAddSingleton(); + services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => sp.GetRequiredService()); + services.TryAddSingleton(sp => + new BotFrameworkHttpAdapter( + new TeamsBotCredentialProvider( + clientId: globalSettings.Teams.ClientId, + clientSecret: globalSettings.Teams.ClientSecret + ) + ) + ); + } + else + { + services.TryAddSingleton(); + } + + return services; + } + public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { @@ -913,6 +943,7 @@ public static class ServiceCollectionExtensions // Add services in support of handlers services.AddSlackService(globalSettings); + services.AddTeamsService(globalSettings); services.TryAddSingleton(TimeProvider.System); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); services.AddHttpClient(DatadogIntegrationHandler.HttpClientName); @@ -921,12 +952,14 @@ public static class ServiceCollectionExtensions services.TryAddSingleton, SlackIntegrationHandler>(); services.TryAddSingleton, WebhookIntegrationHandler>(); services.TryAddSingleton, DatadogIntegrationHandler>(); + services.TryAddSingleton, TeamsIntegrationHandler>(); var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings); var slackConfiguration = new SlackListenerConfiguration(globalSettings); var webhookConfiguration = new WebhookListenerConfiguration(globalSettings); var hecConfiguration = new HecListenerConfiguration(globalSettings); var datadogConfiguration = new DatadogListenerConfiguration(globalSettings); + var teamsConfiguration = new TeamsListenerConfiguration(globalSettings); if (IsRabbitMqEnabled(globalSettings)) { @@ -944,6 +977,7 @@ public static class ServiceCollectionExtensions services.AddRabbitMqIntegration(webhookConfiguration); services.AddRabbitMqIntegration(hecConfiguration); services.AddRabbitMqIntegration(datadogConfiguration); + services.AddRabbitMqIntegration(teamsConfiguration); } if (IsAzureServiceBusEnabled(globalSettings)) @@ -967,6 +1001,7 @@ public static class ServiceCollectionExtensions services.AddAzureServiceBusIntegration(webhookConfiguration); services.AddAzureServiceBusIntegration(hecConfiguration); services.AddAzureServiceBusIntegration(datadogConfiguration); + services.AddAzureServiceBusIntegration(teamsConfiguration); } return services; diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId.sql new file mode 100644 index 0000000000..8e2102772b --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId] + @TenantId NVARCHAR(200), + @TeamId NVARCHAR(200) +AS +BEGIN + SET NOCOUNT ON; + +SELECT TOP 1 * +FROM [dbo].[OrganizationIntegrationView] + CROSS APPLY OPENJSON([Configuration], '$.Teams') + WITH ( TeamId NVARCHAR(MAX) '$.id' ) t +WHERE [Type] = 7 + AND JSON_VALUE([Configuration], '$.TenantId') = @TenantId + AND t.TeamId = @TeamId + AND JSON_VALUE([Configuration], '$.ChannelId') IS NULL + AND JSON_VALUE([Configuration], '$.ServiceUrl') IS NULL; +END diff --git a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs index 376fb01493..61d3486c51 100644 --- a/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/SlackIntegrationControllerTests.cs @@ -34,7 +34,7 @@ public class SlackIntegrationControllerTests integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -60,7 +60,7 @@ public class SlackIntegrationControllerTests integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .GetByIdAsync(integration.Id) @@ -80,7 +80,7 @@ public class SlackIntegrationControllerTests integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .GetByIdAsync(integration.Id) @@ -99,13 +99,13 @@ public class SlackIntegrationControllerTests { sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) .Returns(_slackToken); - await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, String.Empty)); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validSlackCode, string.Empty)); } [Theory, BitAutoData] @@ -116,7 +116,7 @@ public class SlackIntegrationControllerTests var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc)); sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -135,7 +135,7 @@ public class SlackIntegrationControllerTests { sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -147,7 +147,7 @@ public class SlackIntegrationControllerTests } [Theory, BitAutoData] - public async Task CreateAsync_StateHasWrongOgranizationHash_ThrowsNotFound( + public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound( SutProvider sutProvider, OrganizationIntegration integration, OrganizationIntegration wrongOrgIntegration) @@ -156,7 +156,7 @@ public class SlackIntegrationControllerTests sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -179,7 +179,7 @@ public class SlackIntegrationControllerTests integration.Configuration = "{}"; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -201,7 +201,7 @@ public class SlackIntegrationControllerTests integration.Configuration = null; sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns("https://localhost"); sutProvider.GetDependency() .ObtainTokenViaOAuth(_validSlackCode, Arg.Any()) @@ -224,7 +224,7 @@ public class SlackIntegrationControllerTests sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(integration.OrganizationId) @@ -260,7 +260,7 @@ public class SlackIntegrationControllerTests sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) @@ -291,7 +291,7 @@ public class SlackIntegrationControllerTests sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) @@ -316,7 +316,7 @@ public class SlackIntegrationControllerTests sutProvider.Sut.Url = Substitute.For(); sutProvider.Sut.Url - .RouteUrl(Arg.Is(c => c.RouteName == nameof(SlackIntegrationController.CreateAsync))) + .RouteUrl(Arg.Is(c => c.RouteName == "SlackIntegration_Create")) .Returns(expectedUrl); sutProvider.GetDependency() .OrganizationOwner(organizationId) diff --git a/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs new file mode 100644 index 0000000000..3af2affdd8 --- /dev/null +++ b/test/Api.Test/AdminConsole/Controllers/TeamsIntegrationControllerTests.cs @@ -0,0 +1,392 @@ +#nullable enable + +using Bit.Api.AdminConsole.Controllers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Teams; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Bot.Builder; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Controllers; + +[ControllerCustomize(typeof(TeamsIntegrationController))] +[SutProviderCustomize] +public class TeamsIntegrationControllerTests +{ + private const string _teamsToken = "test-token"; + private const string _validTeamsCode = "A_test_code"; + + [Theory, BitAutoData] + public async Task CreateAsync_AllParamsProvided_Succeeds( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Teams; + integration.Configuration = null; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + sutProvider.GetDependency() + .GetJoinedTeamsAsync(_teamsToken) + .Returns([ + new TeamInfo() { DisplayName = "Test Team", Id = Guid.NewGuid().ToString(), TenantId = Guid.NewGuid().ToString() } + ]); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + var requestAction = await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString()); + + await sutProvider.GetDependency().Received(1) + .UpsertAsync(Arg.Any()); + Assert.IsType(requestAction); + } + + [Theory, BitAutoData] + public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Teams; + integration.Configuration = null; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.CreateAsync(string.Empty, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_NoTeamsFound_ThrowsBadRequest( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Teams; + integration.Configuration = null; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + sutProvider.GetDependency() + .GetJoinedTeamsAsync(_teamsToken) + .Returns([]); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_TeamsServiceReturnsEmptyToken_ThrowsBadRequest( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Teams; + integration.Configuration = null; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(string.Empty); + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateEmpty_ThrowsNotFound( + SutProvider sutProvider) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, string.Empty)); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateExpired_ThrowsNotFound( + SutProvider sutProvider, + OrganizationIntegration integration) + { + var timeProvider = new FakeTimeProvider(new DateTime(2024, 4, 3, 2, 1, 0, DateTimeKind.Utc)); + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + var state = IntegrationOAuthState.FromIntegration(integration, timeProvider); + timeProvider.Advance(TimeSpan.FromMinutes(30)); + + sutProvider.SetDependency(timeProvider); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateHasNonexistentIntegration_ThrowsNotFound( + SutProvider sutProvider, + OrganizationIntegration integration) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateHasWrongOrganizationHash_ThrowsNotFound( + SutProvider sutProvider, + OrganizationIntegration integration, + OrganizationIntegration wrongOrgIntegration) + { + wrongOrgIntegration.Id = integration.Id; + wrongOrgIntegration.Type = IntegrationType.Teams; + wrongOrgIntegration.Configuration = null; + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(wrongOrgIntegration); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateHasNonEmptyIntegration_ThrowsNotFound( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Teams; + integration.Configuration = "{}"; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task CreateAsync_StateHasNonTeamsIntegration_ThrowsNotFound( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Type = IntegrationType.Hec; + integration.Configuration = null; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns("https://localhost"); + sutProvider.GetDependency() + .ObtainTokenViaOAuth(_validTeamsCode, Arg.Any()) + .Returns(_teamsToken); + sutProvider.GetDependency() + .GetByIdAsync(integration.Id) + .Returns(integration); + + var state = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.CreateAsync(_validTeamsCode, state.ToString())); + } + + [Theory, BitAutoData] + public async Task RedirectAsync_Success( + SutProvider sutProvider, + OrganizationIntegration integration) + { + integration.Configuration = null; + var expectedUrl = "https://localhost/"; + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns(expectedUrl); + sutProvider.GetDependency() + .OrganizationOwner(integration.OrganizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(integration.OrganizationId) + .Returns([]); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(integration); + sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); + + var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + var requestAction = await sutProvider.Sut.RedirectAsync(integration.OrganizationId); + + Assert.IsType(requestAction); + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Any()); + sutProvider.GetDependency().Received(1).GetRedirectUrl(Arg.Any(), expectedState.ToString()); + } + + [Theory, BitAutoData] + public async Task RedirectAsync_IntegrationAlreadyExistsWithNullConfig_Success( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration integration) + { + integration.OrganizationId = organizationId; + integration.Configuration = null; + integration.Type = IntegrationType.Teams; + var expectedUrl = "https://localhost/"; + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns(expectedUrl); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId) + .Returns([integration]); + sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); + + var requestAction = await sutProvider.Sut.RedirectAsync(organizationId); + + var expectedState = IntegrationOAuthState.FromIntegration(integration, sutProvider.GetDependency()); + + Assert.IsType(requestAction); + sutProvider.GetDependency().Received(1).GetRedirectUrl(Arg.Any(), expectedState.ToString()); + } + + [Theory, BitAutoData] + public async Task RedirectAsync_IntegrationAlreadyExistsWithConfig_ThrowsBadRequest( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration integration) + { + integration.OrganizationId = organizationId; + integration.Configuration = "{}"; + integration.Type = IntegrationType.Teams; + var expectedUrl = "https://localhost/"; + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns(expectedUrl); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId) + .Returns([integration]); + sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(expectedUrl); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); + } + + [Theory, BitAutoData] + public async Task RedirectAsync_TeamsServiceReturnsEmpty_ThrowsNotFound( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration integration) + { + integration.OrganizationId = organizationId; + integration.Configuration = null; + var expectedUrl = "https://localhost/"; + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.Sut.Url + .RouteUrl(Arg.Is(c => c.RouteName == "TeamsIntegration_Create")) + .Returns(expectedUrl); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId) + .Returns([]); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(integration); + sutProvider.GetDependency().GetRedirectUrl(Arg.Any(), Arg.Any()).Returns(string.Empty); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); + } + + [Theory, BitAutoData] + public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider sutProvider, + Guid organizationId) + { + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(false); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.RedirectAsync(organizationId)); + } + + [Theory, BitAutoData] + public async Task IncomingPostAsync_ForwardsToBot(SutProvider sutProvider) + { + var adapter = sutProvider.GetDependency(); + var bot = sutProvider.GetDependency(); + + await sutProvider.Sut.IncomingPostAsync(); + await adapter.Received(1).ProcessAsync(Arg.Any(), Arg.Any(), bot); + } +} diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 74fe75a9d7..8a75db9da8 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -39,7 +39,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests [Theory] [InlineData(data: "")] [InlineData(data: " ")] - public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config) + public void IsValidForType_EmptyNonNullConfiguration_ReturnsFalse(string? config) { var model = new OrganizationIntegrationConfigurationRequestModel { @@ -48,10 +48,12 @@ public class OrganizationIntegrationConfigurationRequestModelTests }; Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); + Assert.False(condition: model.IsValidForType(IntegrationType.Datadog)); + Assert.False(condition: model.IsValidForType(IntegrationType.Teams)); } [Fact] - public void IsValidForType_NullHecConfiguration_ReturnsTrue() + public void IsValidForType_NullConfiguration_ReturnsTrue() { var model = new OrganizationIntegrationConfigurationRequestModel { @@ -60,32 +62,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests }; Assert.True(condition: model.IsValidForType(IntegrationType.Hec)); - } - - [Theory] - [InlineData(data: "")] - [InlineData(data: " ")] - public void IsValidForType_EmptyNonNullDatadogConfiguration_ReturnsFalse(string? config) - { - var model = new OrganizationIntegrationConfigurationRequestModel - { - Configuration = config, - Template = "template" - }; - - Assert.False(condition: model.IsValidForType(IntegrationType.Datadog)); - } - - [Fact] - public void IsValidForType_NullDatadogConfiguration_ReturnsTrue() - { - var model = new OrganizationIntegrationConfigurationRequestModel - { - Configuration = null, - Template = "template" - }; - Assert.True(condition: model.IsValidForType(IntegrationType.Datadog)); + Assert.True(condition: model.IsValidForType(IntegrationType.Teams)); } [Theory] @@ -107,6 +85,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.False(condition: model.IsValidForType(IntegrationType.Slack)); Assert.False(condition: model.IsValidForType(IntegrationType.Webhook)); Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); + Assert.False(condition: model.IsValidForType(IntegrationType.Datadog)); + Assert.False(condition: model.IsValidForType(IntegrationType.Teams)); } [Fact] @@ -121,6 +101,8 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.False(condition: model.IsValidForType(IntegrationType.Slack)); Assert.False(condition: model.IsValidForType(IntegrationType.Webhook)); Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); + Assert.False(condition: model.IsValidForType(IntegrationType.Datadog)); + Assert.False(condition: model.IsValidForType(IntegrationType.Teams)); } diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs index 81927a1bfe..1303e5fe89 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs @@ -57,6 +57,22 @@ public class OrganizationIntegrationRequestModelTests Assert.Contains("cannot be created directly", results[0].ErrorMessage); } + [Fact] + public void Validate_Teams_ReturnsCannotBeCreatedDirectlyError() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Teams, + Configuration = null + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Single(results); + Assert.Contains(nameof(model.Type), results[0].MemberNames); + Assert.Contains("cannot be created directly", results[0].ErrorMessage); + } + [Fact] public void Validate_Webhook_WithNullConfiguration_ReturnsNoErrors() { diff --git a/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs b/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs index babdf3894d..28bc07de38 100644 --- a/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModelTests.cs @@ -1,8 +1,11 @@ #nullable enable +using System.Text.Json; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; +using Bit.Core.Models.Teams; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -58,6 +61,46 @@ public class OrganizationIntegrationResponseModelTests Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status); } + [Theory, BitAutoData] + public void Status_Teams_NullConfig_ReturnsInitiated(OrganizationIntegration oi) + { + oi.Type = IntegrationType.Teams; + oi.Configuration = null; + + var model = new OrganizationIntegrationResponseModel(oi); + + Assert.Equal(OrganizationIntegrationStatus.Initiated, model.Status); + } + + [Theory, BitAutoData] + public void Status_Teams_WithTenantAndTeamsConfig_ReturnsInProgress(OrganizationIntegration oi) + { + oi.Type = IntegrationType.Teams; + oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration( + TenantId: "tenant", Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }] + )); + + var model = new OrganizationIntegrationResponseModel(oi); + + Assert.Equal(OrganizationIntegrationStatus.InProgress, model.Status); + } + + [Theory, BitAutoData] + public void Status_Teams_WithCompletedConfig_ReturnsCompleted(OrganizationIntegration oi) + { + oi.Type = IntegrationType.Teams; + oi.Configuration = JsonSerializer.Serialize(new TeamsIntegration( + TenantId: "tenant", + Teams: [new TeamInfo() { DisplayName = "Team", Id = "TeamId", TenantId = "tenant" }], + ServiceUrl: new Uri("https://example.com"), + ChannelId: "channellId" + )); + + var model = new OrganizationIntegrationResponseModel(oi); + + Assert.Equal(OrganizationIntegrationStatus.Completed, model.Status); + } + [Theory, BitAutoData] public void Status_Webhook_AlwaysCompleted(OrganizationIntegration oi) { diff --git a/test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs b/test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs new file mode 100644 index 0000000000..d3d433727f --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/Teams/TeamsBotCredentialProviderTests.cs @@ -0,0 +1,56 @@ +using Bit.Core.AdminConsole.Models.Teams; +using Microsoft.Bot.Connector.Authentication; +using Xunit; + +namespace Bit.Core.Test.Models.Data.Teams; + +public class TeamsBotCredentialProviderTests +{ + private string _clientId = "client id"; + private string _clientSecret = "client secret"; + + [Fact] + public async Task IsValidAppId_MustMatchClientId() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + + Assert.True(await sut.IsValidAppIdAsync(_clientId)); + Assert.False(await sut.IsValidAppIdAsync("Different id")); + } + + [Fact] + public async Task GetAppPasswordAsync_MatchingClientId_ReturnsClientSecret() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + var password = await sut.GetAppPasswordAsync(_clientId); + Assert.Equal(_clientSecret, password); + } + + [Fact] + public async Task GetAppPasswordAsync_NotMatchingClientId_ReturnsNull() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + Assert.Null(await sut.GetAppPasswordAsync("Different id")); + } + + [Fact] + public async Task IsAuthenticationDisabledAsync_ReturnsFalse() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + Assert.False(await sut.IsAuthenticationDisabledAsync()); + } + + [Fact] + public async Task ValidateIssuerAsync_ExpectedIssuer_ReturnsTrue() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + Assert.True(await sut.ValidateIssuerAsync(AuthenticationConstants.ToBotFromChannelTokenIssuer)); + } + + [Fact] + public async Task ValidateIssuerAsync_UnexpectedIssuer_ReturnsFalse() + { + var sut = new TeamsBotCredentialProvider(_clientId, _clientSecret); + Assert.False(await sut.ValidateIssuerAsync("unexpected issuer")); + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs index 98cf974df8..715bffaab1 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs @@ -5,17 +5,6 @@ namespace Bit.Core.Test.Services; public class IntegrationTypeTests { - [Fact] - public void ToRoutingKey_Slack_Succeeds() - { - Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey()); - } - [Fact] - public void ToRoutingKey_Webhook_Succeeds() - { - Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey()); - } - [Fact] public void ToRoutingKey_CloudBillingSync_ThrowsException() { @@ -27,4 +16,34 @@ public class IntegrationTypeTests { Assert.Throws(() => IntegrationType.Scim.ToRoutingKey()); } + + [Fact] + public void ToRoutingKey_Slack_Succeeds() + { + Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Webhook_Succeeds() + { + Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Hec_Succeeds() + { + Assert.Equal("hec", IntegrationType.Hec.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Datadog_Succeeds() + { + Assert.Equal("datadog", IntegrationType.Datadog.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Teams_Succeeds() + { + Assert.Equal("teams", IntegrationType.Teams.ToRoutingKey()); + } } diff --git a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs index 2d0ca2433a..48dd9c490e 100644 --- a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs @@ -296,6 +296,18 @@ public class SlackServiceTests Assert.Equal("test-access-token", result); } + [Theory] + [InlineData("test-code", "")] + [InlineData("", "https://example.com/callback")] + [InlineData("", "")] + public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenCodeOrRedirectUrlIsEmpty(string code, string redirectUrl) + { + var sutProvider = GetSutProvider(); + var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl); + + Assert.Equal(string.Empty, result); + } + [Fact] public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenErrorResponse() { diff --git a/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs new file mode 100644 index 0000000000..b744a6aa69 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/TeamsIntegrationHandlerTests.cs @@ -0,0 +1,126 @@ +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.Rest; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class TeamsIntegrationHandlerTests +{ + private readonly ITeamsService _teamsService = Substitute.For(); + private readonly string _channelId = "C12345"; + private readonly Uri _serviceUrl = new Uri("http://localhost"); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_teamsService) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } + + + [Theory, BitAutoData] + public async Task HandleAsync_HttpExceptionNonRetryable_ReturnsFalseAndNotRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpOperationException("Server error") + { + Response = new HttpResponseMessageWrapper( + new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden), + "Forbidden" + ) + } + ); + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } + + [Theory, BitAutoData] + public async Task HandleAsync_HttpExceptionRetryable_ReturnsFalseAndRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new HttpOperationException("Server error") + { + Response = new HttpResponseMessageWrapper( + new HttpResponseMessage(System.Net.HttpStatusCode.TooManyRequests), + "Too Many Requests" + ) + } + ); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } + + [Theory, BitAutoData] + public async Task HandleAsync_UnknownException_ReturnsFalseAndNotRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new TeamsIntegrationConfigurationDetails(_channelId, _serviceUrl); + + sutProvider.GetDependency() + .SendMessageToChannelAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .ThrowsAsync(new Exception("Unknown error")); + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendMessageToChannelAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_serviceUrl)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)) + ); + } +} diff --git a/test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs b/test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs new file mode 100644 index 0000000000..17d65f3237 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/TeamsServiceTests.cs @@ -0,0 +1,289 @@ +#nullable enable + +using System.Net; +using System.Text.Json; +using System.Web; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Models.Teams; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class TeamsServiceTests +{ + private readonly MockedHttpMessageHandler _handler; + private readonly HttpClient _httpClient; + + public TeamsServiceTests() + { + _handler = new MockedHttpMessageHandler(); + _httpClient = _handler.ToHttpClient(); + } + + private SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(TeamsService.HttpClientName).Returns(_httpClient); + + var globalSettings = Substitute.For(); + globalSettings.Teams.LoginBaseUrl.Returns("https://login.example.com"); + globalSettings.Teams.GraphBaseUrl.Returns("https://graph.example.com"); + + return new SutProvider() + .SetDependency(clientFactory) + .SetDependency(globalSettings) + .Create(); + } + + [Fact] + public void GetRedirectUrl_ReturnsCorrectUrl() + { + var sutProvider = GetSutProvider(); + var clientId = sutProvider.GetDependency().Teams.ClientId; + var scopes = sutProvider.GetDependency().Teams.Scopes; + var callbackUrl = "https://example.com/callback"; + var state = Guid.NewGuid().ToString(); + var result = sutProvider.Sut.GetRedirectUrl(callbackUrl, state); + + var uri = new Uri(result); + var query = HttpUtility.ParseQueryString(uri.Query); + + Assert.Equal(clientId, query["client_id"]); + Assert.Equal(scopes, query["scope"]); + Assert.Equal(callbackUrl, query["redirect_uri"]); + Assert.Equal(state, query["state"]); + Assert.Equal("login.example.com", uri.Host); + Assert.Equal("/common/oauth2/v2.0/authorize", uri.AbsolutePath); + } + + [Fact] + public async Task ObtainTokenViaOAuth_Success_ReturnsAccessToken() + { + var sutProvider = GetSutProvider(); + var jsonResponse = JsonSerializer.Serialize(new + { + access_token = "test-access-token" + }); + + _handler.When("https://login.example.com/common/oauth2/v2.0/token") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent(jsonResponse)); + + var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); + + Assert.Equal("test-access-token", result); + } + + [Theory] + [InlineData("test-code", "")] + [InlineData("", "https://example.com/callback")] + [InlineData("", "")] + public async Task ObtainTokenViaOAuth_CodeOrRedirectUrlIsEmpty_ReturnsEmptyString(string code, string redirectUrl) + { + var sutProvider = GetSutProvider(); + var result = await sutProvider.Sut.ObtainTokenViaOAuth(code, redirectUrl); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public async Task ObtainTokenViaOAuth_HttpFailure_ReturnsEmptyString() + { + var sutProvider = GetSutProvider(); + _handler.When("https://login.example.com/common/oauth2/v2.0/token") + .RespondWith(HttpStatusCode.InternalServerError) + .WithContent(new StringContent(string.Empty)); + + var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public async Task ObtainTokenViaOAuth_UnknownResponse_ReturnsEmptyString() + { + var sutProvider = GetSutProvider(); + + _handler.When("https://login.example.com/common/oauth2/v2.0/token") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent("Not an expected response")); + + var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback"); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public async Task GetJoinedTeamsAsync_Success_ReturnsTeams() + { + var sutProvider = GetSutProvider(); + + var jsonResponse = JsonSerializer.Serialize(new + { + value = new[] + { + new { id = "team1", displayName = "Team One" }, + new { id = "team2", displayName = "Team Two" } + } + }); + + _handler.When("https://graph.example.com/me/joinedTeams") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent(jsonResponse)); + + var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); + + Assert.Equal(2, result.Count); + Assert.Contains(result, t => t is { Id: "team1", DisplayName: "Team One" }); + Assert.Contains(result, t => t is { Id: "team2", DisplayName: "Team Two" }); + } + + [Fact] + public async Task GetJoinedTeamsAsync_ServerReturnsEmpty_ReturnsEmptyList() + { + var sutProvider = GetSutProvider(); + + var jsonResponse = JsonSerializer.Serialize(new { value = (object?)null }); + + _handler.When("https://graph.example.com/me/joinedTeams") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent(jsonResponse)); + + var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public async Task GetJoinedTeamsAsync_ServerErrorCode_ReturnsEmptyList() + { + var sutProvider = GetSutProvider(); + + _handler.When("https://graph.example.com/me/joinedTeams") + .RespondWith(HttpStatusCode.Unauthorized) + .WithContent(new StringContent("Unauthorized")); + + var result = await sutProvider.Sut.GetJoinedTeamsAsync("fake-access-token"); + + Assert.NotNull(result); + Assert.Empty(result); + } + + [Theory, BitAutoData] + public async Task HandleIncomingAppInstall_Success_UpdatesTeamsIntegration( + OrganizationIntegration integration) + { + var sutProvider = GetSutProvider(); + var tenantId = Guid.NewGuid().ToString(); + var teamId = Guid.NewGuid().ToString(); + var conversationId = Guid.NewGuid().ToString(); + var serviceUrl = new Uri("https://localhost"); + var initiatedConfiguration = new TeamsIntegration(TenantId: tenantId, Teams: + [ + new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId }, + new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "other team", TenantId = tenantId }, + new TeamInfo() { Id = Guid.NewGuid().ToString(), DisplayName = "third team", TenantId = tenantId } + ]); + integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration); + + sutProvider.GetDependency() + .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId) + .Returns(integration); + + OrganizationIntegration? capturedIntegration = null; + await sutProvider.GetDependency() + .UpsertAsync(Arg.Do(x => capturedIntegration = x)); + + await sutProvider.Sut.HandleIncomingAppInstallAsync( + conversationId: conversationId, + serviceUrl: serviceUrl, + teamId: teamId, + tenantId: tenantId + ); + + await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId); + Assert.NotNull(capturedIntegration); + var configuration = JsonSerializer.Deserialize(capturedIntegration.Configuration ?? string.Empty); + Assert.NotNull(configuration); + Assert.NotNull(configuration.ServiceUrl); + Assert.Equal(serviceUrl, configuration.ServiceUrl); + Assert.Equal(conversationId, configuration.ChannelId); + } + + [Fact] + public async Task HandleIncomingAppInstall_NoIntegrationMatched_DoesNothing() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.HandleIncomingAppInstallAsync( + conversationId: "conversationId", + serviceUrl: new Uri("https://localhost"), + teamId: "teamId", + tenantId: "tenantId" + ); + + await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId"); + await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleIncomingAppInstall_MatchedIntegrationAlreadySetup_DoesNothing( + OrganizationIntegration integration) + { + var sutProvider = GetSutProvider(); + var tenantId = Guid.NewGuid().ToString(); + var teamId = Guid.NewGuid().ToString(); + var initiatedConfiguration = new TeamsIntegration( + TenantId: tenantId, + Teams: [new TeamInfo() { Id = teamId, DisplayName = "test team", TenantId = tenantId }], + ChannelId: "ChannelId", + ServiceUrl: new Uri("https://localhost") + ); + integration.Configuration = JsonSerializer.Serialize(initiatedConfiguration); + + sutProvider.GetDependency() + .GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId) + .Returns(integration); + + await sutProvider.Sut.HandleIncomingAppInstallAsync( + conversationId: "conversationId", + serviceUrl: new Uri("https://localhost"), + teamId: teamId, + tenantId: tenantId + ); + + await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId(tenantId, teamId); + await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleIncomingAppInstall_MatchedIntegrationWithMissingConfiguration_DoesNothing( + OrganizationIntegration integration) + { + var sutProvider = GetSutProvider(); + integration.Configuration = null; + + sutProvider.GetDependency() + .GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId") + .Returns(integration); + + await sutProvider.Sut.HandleIncomingAppInstallAsync( + conversationId: "conversationId", + serviceUrl: new Uri("https://localhost"), + teamId: "teamId", + tenantId: "tenantId" + ); + + await sutProvider.GetDependency().Received(1).GetByTeamsConfigurationTenantIdTeamId("tenantId", "teamId"); + await sutProvider.GetDependency().DidNotReceive().UpsertAsync(Arg.Any()); + } +} diff --git a/util/Migrator/DbScripts/2025-10-3_00_AddOrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamIdStoredProcedure.sql b/util/Migrator/DbScripts/2025-10-3_00_AddOrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamIdStoredProcedure.sql new file mode 100644 index 0000000000..16ac419cad --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-3_00_AddOrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamIdStoredProcedure.sql @@ -0,0 +1,18 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegration_ReadByTeamsConfigurationTenantIdTeamId] + @TenantId NVARCHAR(200), + @TeamId NVARCHAR(200) +AS +BEGIN + SET NOCOUNT ON; + +SELECT TOP 1 * +FROM [dbo].[OrganizationIntegrationView] + CROSS APPLY OPENJSON([Configuration], '$.Teams') + WITH ( TeamId NVARCHAR(MAX) '$.id' ) t +WHERE [Type] = 7 + AND JSON_VALUE([Configuration], '$.TenantId') = @TenantId + AND t.TeamId = @TeamId + AND JSON_VALUE([Configuration], '$.ChannelId') IS NULL + AND JSON_VALUE([Configuration], '$.ServiceUrl') IS NULL; +END +GO From 60721041531730378359d787ff09e8251b4655bf Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 10 Oct 2025 11:23:02 -0400 Subject: [PATCH 11/52] Ac/pm 25823/vnext policy upsert pattern (#6426) --- .../Implementations/VNextSavePolicyCommand.cs | 208 ++++++++ .../Policies/Models/PolicyUpdate.cs | 2 + .../PolicyServiceCollectionExtensions.cs | 4 + .../IEnforceDependentPoliciesEvent.cs | 12 + .../Interfaces/IOnPolicyPostUpdateEvent.cs | 18 + .../Interfaces/IOnPolicyPreUpdateEvent.cs | 17 + .../Interfaces/IPolicyEventHandlerFactory.cs | 30 ++ .../Interfaces/IPolicyUpdateEvent.cs | 11 + .../Interfaces/IPolicyValidationEvent.cs | 19 + .../Interfaces/IVNextSavePolicyCommand.cs | 34 ++ .../PolicyEventHandlerHandlerFactory.cs | 33 ++ .../PolicyEventHandlerHandlerFactoryTests.cs | 124 +++++ .../Policies/PolicyUpdateEventFixtures.cs | 37 ++ .../Policies/VNextSavePolicyCommandTests.cs | 471 ++++++++++++++++++ 14 files changed, 1020 insertions(+) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPostUpdateEvent.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyEventHandlerFactory.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IVNextSavePolicyCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/PolicyEventHandlerHandlerFactory.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlerHandlerFactoryTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEventFixtures.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs new file mode 100644 index 0000000000..1a2b78fc8a --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs @@ -0,0 +1,208 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; + +public class VNextSavePolicyCommand( + IApplicationCacheService applicationCacheService, + IEventService eventService, + IPolicyRepository policyRepository, + IEnumerable policyValidationEventHandlers, + TimeProvider timeProvider, + IPolicyEventHandlerFactory policyEventHandlerFactory) + : IVNextSavePolicyCommand +{ + private readonly IReadOnlyDictionary _policyValidationEvents = MapToDictionary(policyValidationEventHandlers); + + private static Dictionary MapToDictionary(IEnumerable policyValidationEventHandlers) + { + var policyValidationEventsDict = new Dictionary(); + foreach (var policyValidationEvent in policyValidationEventHandlers) + { + if (!policyValidationEventsDict.TryAdd(policyValidationEvent.Type, policyValidationEvent)) + { + throw new Exception($"Duplicate PolicyValidationEvent for {policyValidationEvent.Type} policy."); + } + } + return policyValidationEventsDict; + } + + public async Task SaveAsync(SavePolicyModel policyRequest) + { + var policyUpdateRequest = policyRequest.PolicyUpdate; + var organizationId = policyUpdateRequest.OrganizationId; + + await EnsureOrganizationCanUsePolicyAsync(organizationId); + + var savedPoliciesDict = await GetCurrentPolicyStateAsync(organizationId); + + var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdateRequest.Type); + + ValidatePolicyDependencies(policyUpdateRequest, currentPolicy, savedPoliciesDict); + + await ValidateTargetedPolicyAsync(policyRequest, currentPolicy); + + await ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy); + + var upsertedPolicy = await UpsertPolicyAsync(policyUpdateRequest); + + await eventService.LogPolicyEventAsync(upsertedPolicy, EventType.Policy_Updated); + + await ExecutePostUpsertSideEffectAsync(policyRequest, upsertedPolicy, currentPolicy); + + return upsertedPolicy; + } + + private async Task EnsureOrganizationCanUsePolicyAsync(Guid organizationId) + { + var org = await applicationCacheService.GetOrganizationAbilityAsync(organizationId); + if (org == null) + { + throw new BadRequestException("Organization not found"); + } + + if (!org.UsePolicies) + { + throw new BadRequestException("This organization cannot use policies."); + } + } + + private async Task UpsertPolicyAsync(PolicyUpdate policyUpdateRequest) + { + var policy = await policyRepository.GetByOrganizationIdTypeAsync(policyUpdateRequest.OrganizationId, policyUpdateRequest.Type) + ?? new Policy + { + OrganizationId = policyUpdateRequest.OrganizationId, + Type = policyUpdateRequest.Type, + CreationDate = timeProvider.GetUtcNow().UtcDateTime + }; + + policy.Enabled = policyUpdateRequest.Enabled; + policy.Data = policyUpdateRequest.Data; + policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime; + + await policyRepository.UpsertAsync(policy); + + return policy; + } + + private async Task ValidateTargetedPolicyAsync(SavePolicyModel policyRequest, + Policy? currentPolicy) + { + await ExecutePolicyEventAsync( + policyRequest.PolicyUpdate.Type, + async validator => + { + var validationError = await validator.ValidateAsync(policyRequest, currentPolicy); + if (!string.IsNullOrEmpty(validationError)) + { + throw new BadRequestException(validationError); + } + }); + } + + private void ValidatePolicyDependencies( + PolicyUpdate policyUpdateRequest, + Policy? currentPolicy, + Dictionary savedPoliciesDict) + { + var result = policyEventHandlerFactory.GetHandler(policyUpdateRequest.Type); + + result.Switch( + validator => + { + var isCurrentlyEnabled = currentPolicy?.Enabled == true; + + switch (policyUpdateRequest.Enabled) + { + case true when !isCurrentlyEnabled: + ValidateEnablingRequirements(validator, savedPoliciesDict); + return; + case false when isCurrentlyEnabled: + ValidateDisablingRequirements(validator, policyUpdateRequest.Type, savedPoliciesDict); + break; + } + }, + _ => { }); + } + + private void ValidateDisablingRequirements( + IEnforceDependentPoliciesEvent validator, + PolicyType policyType, + Dictionary savedPoliciesDict) + { + var dependentPolicyTypes = _policyValidationEvents.Values + .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType)) + .Select(otherValidator => otherValidator.Type) + .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) && + savedPolicy.Enabled) + .ToList(); + + switch (dependentPolicyTypes) + { + case { Count: 1 }: + throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy."); + case { Count: > 1 }: + throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy."); + } + } + + private static void ValidateEnablingRequirements( + IEnforceDependentPoliciesEvent validator, + Dictionary savedPoliciesDict) + { + var missingRequiredPolicyTypes = validator.RequiredPolicies + .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) + .ToList(); + + if (missingRequiredPolicyTypes.Count != 0) + { + throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy."); + } + } + + private async Task ExecutePreUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy? currentPolicy) + { + await ExecutePolicyEventAsync( + policyRequest.PolicyUpdate.Type, + handler => handler.ExecutePreUpsertSideEffectAsync(policyRequest, currentPolicy)); + } + private async Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState) + { + await ExecutePolicyEventAsync( + policyRequest.PolicyUpdate.Type, + handler => handler.ExecutePostUpsertSideEffectAsync( + policyRequest, + postUpsertedPolicyState, + previousPolicyState)); + } + + private async Task ExecutePolicyEventAsync(PolicyType type, Func func) where T : IPolicyUpdateEvent + { + var handler = policyEventHandlerFactory.GetHandler(type); + + await handler.Match( + async h => await func(h), + _ => Task.CompletedTask + ); + } + + private async Task> GetCurrentPolicyStateAsync(Guid organizationId) + { + var savedPolicies = await policyRepository.GetManyByOrganizationIdAsync(organizationId); + // Note: policies may be missing from this dict if they have never been enabled + var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); + return savedPoliciesDict; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs index d1a52f0080..cad786234c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/PolicyUpdate.cs @@ -16,6 +16,8 @@ public record PolicyUpdate public PolicyType Type { get; set; } public string? Data { get; set; } public bool Enabled { get; set; } + + [Obsolete("Please use SavePolicyModel.PerformedBy instead.")] public IActingUser? PerformedBy { get; set; } public T GetDataModel() where T : IPolicyDataModel, new() diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 5433d70410..f35ff87424 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; @@ -13,7 +15,9 @@ public static class PolicyServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddPolicyValidators(); services.AddPolicyRequirements(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs new file mode 100644 index 0000000000..798417ae7c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IEnforceDependentPoliciesEvent.cs @@ -0,0 +1,12 @@ +using Bit.Core.AdminConsole.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +public interface IEnforceDependentPoliciesEvent : IPolicyUpdateEvent +{ + /// + /// PolicyTypes that must be enabled before this policy can be enabled, if any. + /// These dependencies will be checked when this policy is enabled and when any required policy is disabled. + /// + public IEnumerable RequiredPolicies { get; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPostUpdateEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPostUpdateEvent.cs new file mode 100644 index 0000000000..08295bf7fb --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPostUpdateEvent.cs @@ -0,0 +1,18 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +public interface IOnPolicyPostUpdateEvent : IPolicyUpdateEvent +{ + /// + /// Performs side effects after a policy has been upserted. + /// For example, this can be used for cleanup tasks or notifications. + /// + /// The policy save request + /// The policy after it was upserted + /// The policy state before it was updated, if any + public Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs new file mode 100644 index 0000000000..278a17f35e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IOnPolicyPreUpdateEvent.cs @@ -0,0 +1,17 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +public interface IOnPolicyPreUpdateEvent : IPolicyUpdateEvent +{ + /// + /// Performs side effects before a policy is upserted. + /// For example, this can be used to remove non-compliant users from the organization. + /// + /// The policy save request containing the policy update and metadata + /// The current policy, if any + public Task ExecutePreUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy? currentPolicy); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyEventHandlerFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyEventHandlerFactory.cs new file mode 100644 index 0000000000..f44ae867dd --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyEventHandlerFactory.cs @@ -0,0 +1,30 @@ +#nullable enable + +using Bit.Core.AdminConsole.Enums; +using OneOf; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +/// +/// Provides policy-specific event handlers used during the save workflow in . +/// +/// +/// Supported handlers: +/// - for dependency checks +/// - for custom validation +/// - for pre-save logic +/// - for post-save logic +/// +public interface IPolicyEventHandlerFactory +{ + /// + /// Gets the event handler for the given policy type and handler interface. + /// + /// Handler type implementing . + /// The policy type to resolve. + /// + /// — the handler if available, or None if not implemented. + /// + OneOf GetHandler(PolicyType policyType) where T : IPolicyUpdateEvent; +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs new file mode 100644 index 0000000000..ded1a14f1a --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyUpdateEvent.cs @@ -0,0 +1,11 @@ +using Bit.Core.AdminConsole.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +public interface IPolicyUpdateEvent +{ + /// + /// The policy type that the associated handler will handle. + /// + public PolicyType Type { get; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs new file mode 100644 index 0000000000..6d486e1fa0 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IPolicyValidationEvent.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +public interface IPolicyValidationEvent : IPolicyUpdateEvent +{ + /// + /// Performs side effects after a policy is validated but before it is saved. + /// For example, this can be used to remove non-compliant users from the organization. + /// Implementation is optional; by default, it will not perform any side effects. + /// + /// The policy save request containing the policy update and metadata + /// The current policy, if any + public Task ValidateAsync( + SavePolicyModel policyRequest, + Policy? currentPolicy); + +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IVNextSavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IVNextSavePolicyCommand.cs new file mode 100644 index 0000000000..93414539bb --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/Interfaces/IVNextSavePolicyCommand.cs @@ -0,0 +1,34 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Microsoft.Azure.NotificationHubs.Messaging; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; + +/// +/// Handles creating or updating organization policies with validation and side effect execution. +/// +/// +/// Workflow: +/// 1. Validates organization can use policies +/// 2. Validates required and dependent policies +/// 3. Runs policy-specific validation () +/// 4. Executes pre-save logic () +/// 5. Saves the policy +/// 6. Logs the event +/// 7. Executes post-save logic () +/// +public interface IVNextSavePolicyCommand +{ + /// + /// Performs the necessary validations, saves the policy and any side effects + /// + /// Policy data, acting user, and metadata. + /// The saved policy with updated revision and applied changes. + /// + /// Thrown if: + /// - The organization can’t use policies + /// - Dependent policies are missing or block changes + /// - Custom validation fails + /// + Task SaveAsync(SavePolicyModel policyRequest); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/PolicyEventHandlerHandlerFactory.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/PolicyEventHandlerHandlerFactory.cs new file mode 100644 index 0000000000..b1abfb2aaf --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEvents/PolicyEventHandlerHandlerFactory.cs @@ -0,0 +1,33 @@ + +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using OneOf; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents; + +public class PolicyEventHandlerHandlerFactory( + IEnumerable allEventHandlers) : IPolicyEventHandlerFactory +{ + public OneOf GetHandler(PolicyType policyType) where T : IPolicyUpdateEvent + { + var tEventHandlers = allEventHandlers.OfType().ToList(); + + var matchingHandlers = tEventHandlers.Where(h => h.Type == policyType).ToList(); + + if (matchingHandlers.Count > 1) + { + throw new InvalidOperationException( + $"Multiple {nameof(IPolicyUpdateEvent)} handlers of type {typeof(T).Name} found for {nameof(PolicyType)} {policyType}. " + + $"Expected one {typeof(T).Name} handler per {nameof(PolicyType)}."); + } + + var policyTEventHandler = matchingHandlers.SingleOrDefault(); + if (policyTEventHandler is null) + { + return new None(); + } + + return policyTEventHandler; + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlerHandlerFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlerHandlerFactoryTests.cs new file mode 100644 index 0000000000..61d24735b6 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlerHandlerFactoryTests.cs @@ -0,0 +1,124 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using OneOf.Types; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class PolicyEventHandlerHandlerFactoryTests +{ + [Fact] + public void GetHandler_ReturnsHandler_WhenHandlerExists() + { + // Arrange + var expectedHandler = new FakeSingleOrgDependencyEvent(); + var factory = new PolicyEventHandlerHandlerFactory([expectedHandler]); + + // Act + var result = factory.GetHandler(PolicyType.SingleOrg); + + // Assert + Assert.True(result.IsT0); + Assert.Equal(expectedHandler, result.AsT0); + } + + [Fact] + public void GetHandler_ReturnsNone_WhenHandlerDoesNotExist() + { + // Arrange + var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]); + + // Act + var result = factory.GetHandler(PolicyType.RequireSso); + + // Assert + Assert.True(result.IsT1); + Assert.IsType(result.AsT1); + } + + [Fact] + public void GetHandler_ReturnsNone_WhenHandlerTypeDoesNotMatch() + { + // Arrange + var factory = new PolicyEventHandlerHandlerFactory([new FakeSingleOrgDependencyEvent()]); + + // Act + var result = factory.GetHandler(PolicyType.SingleOrg); + + // Assert + Assert.True(result.IsT1); + Assert.IsType(result.AsT1); + } + + [Fact] + public void GetHandler_ReturnsCorrectHandler_WhenMultipleHandlerTypesExist() + { + // Arrange + var dependencyEvent = new FakeSingleOrgDependencyEvent(); + var validationEvent = new FakeSingleOrgValidationEvent(); + var factory = new PolicyEventHandlerHandlerFactory([dependencyEvent, validationEvent]); + + // Act + var dependencyResult = factory.GetHandler(PolicyType.SingleOrg); + var validationResult = factory.GetHandler(PolicyType.SingleOrg); + + // Assert + Assert.True(dependencyResult.IsT0); + Assert.Equal(dependencyEvent, dependencyResult.AsT0); + + Assert.True(validationResult.IsT0); + Assert.Equal(validationEvent, validationResult.AsT0); + } + + [Fact] + public void GetHandler_ReturnsCorrectHandler_WhenMultiplePolicyTypesExist() + { + // Arrange + var singleOrgEvent = new FakeSingleOrgDependencyEvent(); + var requireSsoEvent = new FakeRequireSsoDependencyEvent(); + var factory = new PolicyEventHandlerHandlerFactory([singleOrgEvent, requireSsoEvent]); + + // Act + var singleOrgResult = factory.GetHandler(PolicyType.SingleOrg); + var requireSsoResult = factory.GetHandler(PolicyType.RequireSso); + + // Assert + Assert.True(singleOrgResult.IsT0); + Assert.Equal(singleOrgEvent, singleOrgResult.AsT0); + + Assert.True(requireSsoResult.IsT0); + Assert.Equal(requireSsoEvent, requireSsoResult.AsT0); + } + + [Fact] + public void GetHandler_Throws_WhenDuplicateHandlersExist() + { + // Arrange + var factory = new PolicyEventHandlerHandlerFactory([ + new FakeSingleOrgDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + // Act & Assert + var exception = Assert.Throws(() => + factory.GetHandler(PolicyType.SingleOrg)); + + Assert.Contains("Multiple IPolicyUpdateEvent handlers of type IEnforceDependentPoliciesEvent found for PolicyType SingleOrg", exception.Message); + Assert.Contains("Expected one IEnforceDependentPoliciesEvent handler per PolicyType", exception.Message); + } + + [Fact] + public void GetHandler_ReturnsNone_WhenNoHandlersProvided() + { + // Arrange + var factory = new PolicyEventHandlerHandlerFactory([]); + + // Act + var result = factory.GetHandler(PolicyType.SingleOrg); + + // Assert + Assert.True(result.IsT1); + Assert.IsType(result.AsT1); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEventFixtures.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEventFixtures.cs new file mode 100644 index 0000000000..4c5b23d6e1 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyUpdateEventFixtures.cs @@ -0,0 +1,37 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using NSubstitute; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class FakeSingleOrgDependencyEvent : IEnforceDependentPoliciesEvent +{ + public PolicyType Type => PolicyType.SingleOrg; + public IEnumerable RequiredPolicies => []; +} + +public class FakeRequireSsoDependencyEvent : IEnforceDependentPoliciesEvent +{ + public PolicyType Type => PolicyType.RequireSso; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; +} + +public class FakeVaultTimeoutDependencyEvent : IEnforceDependentPoliciesEvent +{ + public PolicyType Type => PolicyType.MaximumVaultTimeout; + public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; +} + +public class FakeSingleOrgValidationEvent : IPolicyValidationEvent +{ + public PolicyType Type => PolicyType.SingleOrg; + + public readonly Func> ValidateAsyncMock = Substitute.For>>(); + + public Task ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + return ValidateAsyncMock(policyRequest, currentPolicy); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs new file mode 100644 index 0000000000..1510042446 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs @@ -0,0 +1,471 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using OneOf.Types; +using Xunit; +using EventType = Bit.Core.Enums.EventType; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies; + +public class VNextSavePolicyCommandTests +{ + [Theory, BitAutoData] + public async Task SaveAsync_NewPolicy_Success([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) + { + // Arrange + var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); + fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); + var sutProvider = SutProviderFactory( + [new FakeSingleOrgDependencyEvent()], + [fakePolicyValidationEvent]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var newPolicy = new Policy + { + Type = policyUpdate.Type, + OrganizationId = policyUpdate.OrganizationId, + Enabled = false + }; + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([newPolicy]); + + var creationDate = sutProvider.GetDependency().Start; + + // Act + await sutProvider.Sut.SaveAsync(savePolicyModel); + + // Assert + await fakePolicyValidationEvent.ValidateAsyncMock + .Received(1) + .Invoke(Arg.Any(), Arg.Any()); + + await AssertPolicySavedAsync(sutProvider, policyUpdate); + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.CreationDate == creationDate && + p.RevisionDate == creationDate)); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ExistingPolicy_Success( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy currentPolicy) + { + // Arrange + var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); + fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); + var sutProvider = SutProviderFactory( + [new FakeSingleOrgDependencyEvent()], + [fakePolicyValidationEvent]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + .Returns(currentPolicy); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy]); + + // Act + await sutProvider.Sut.SaveAsync(savePolicyModel); + + // Assert + await fakePolicyValidationEvent.ValidateAsyncMock + .Received(1) + .Invoke(Arg.Any(), currentPolicy); + + await AssertPolicySavedAsync(sutProvider, policyUpdate); + + + var revisionDate = sutProvider.GetDependency().Start; + + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(Arg.Is(p => + p.Id == currentPolicy.Id && + p.OrganizationId == currentPolicy.OrganizationId && + p.Type == currentPolicy.Type && + p.CreationDate == currentPolicy.CreationDate && + p.RevisionDate == revisionDate)); + } + + [Fact] + public void Constructor_DuplicatePolicyDependencyEvents_Throws() + { + // Arrange & Act + var exception = Assert.Throws(() => + new VNextSavePolicyCommand( + Substitute.For(), + Substitute.For(), + Substitute.For(), + [new FakeSingleOrgDependencyEvent(), new FakeSingleOrgDependencyEvent()], + Substitute.For(), + Substitute.For())); + + // Assert + Assert.Contains("Duplicate PolicyValidationEvent for SingleOrg policy", exception.Message); + } + + [Theory, BitAutoData] + public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) + { + // Arrange + var sutProvider = SutProviderFactory(); + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(Task.FromResult(null)); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Organization not found", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) + { + // Arrange + var sutProvider = SutProviderFactory(); + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(new OrganizationAbility + { + Id = policyUpdate.OrganizationId, + UsePolicies = false + }); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("cannot use policies", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyIsNull_Throws( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var requireSsoPolicy = new Policy + { + Type = PolicyType.RequireSso, + OrganizationId = policyUpdate.OrganizationId, + Enabled = false + }; + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([requireSsoPolicy]); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyNotEnabled_Throws( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy singleOrgPolicy) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var requireSsoPolicy = new Policy + { + Type = PolicyType.RequireSso, + OrganizationId = policyUpdate.OrganizationId, + Enabled = false + }; + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([singleOrgPolicy, requireSsoPolicy]); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Turn on the Single organization policy because it is required for the Require single sign-on authentication policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_RequiredPolicyEnabled_Success( + [PolicyUpdate(PolicyType.RequireSso)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy singleOrgPolicy) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var requireSsoPolicy = new Policy + { + Type = PolicyType.RequireSso, + OrganizationId = policyUpdate.OrganizationId, + Enabled = false + }; + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([singleOrgPolicy, requireSsoPolicy]); + + // Act + await sutProvider.Sut.SaveAsync(savePolicyModel); + + // Assert + await AssertPolicySavedAsync(sutProvider, policyUpdate); + } + + [Theory, BitAutoData] + public async Task SaveAsync_DependentPolicyIsEnabled_Throws( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy]); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Turn off the Require single sign-on authentication policy because it requires the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_MultipleDependentPoliciesAreEnabled_Throws( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso)] Policy requireSsoPolicy, + [Policy(PolicyType.MaximumVaultTimeout)] Policy vaultTimeoutPolicy) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent(), + new FakeVaultTimeoutDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy, vaultTimeoutPolicy]); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Turn off all of the policies that require the Single organization policy", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + [Theory, BitAutoData] + public async Task SaveAsync_DependentPolicyNotEnabled_Success( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy currentPolicy, + [Policy(PolicyType.RequireSso, false)] Policy requireSsoPolicy) + { + // Arrange + var sutProvider = SutProviderFactory( + [ + new FakeRequireSsoDependencyEvent(), + new FakeSingleOrgDependencyEvent() + ]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy, requireSsoPolicy]); + + // Act + await sutProvider.Sut.SaveAsync(savePolicyModel); + + // Assert + await AssertPolicySavedAsync(sutProvider, policyUpdate); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ThrowsOnValidationError([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate) + { + // Arrange + var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); + fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns("Validation error!"); + var sutProvider = SutProviderFactory( + [new FakeSingleOrgDependencyEvent()], + [fakePolicyValidationEvent]); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var singleOrgPolicy = new Policy + { + Type = PolicyType.SingleOrg, + OrganizationId = policyUpdate.OrganizationId, + Enabled = false + }; + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([singleOrgPolicy]); + + // Act + var badRequestException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(savePolicyModel)); + + // Assert + Assert.Contains("Validation error!", badRequestException.Message, StringComparison.OrdinalIgnoreCase); + await AssertPolicyNotSavedAsync(sutProvider); + } + + /// + /// Returns a new SutProvider with the PolicyDependencyEvents registered in the Sut. + /// + private static SutProvider SutProviderFactory( + IEnumerable? policyDependencyEvents = null, + IEnumerable? policyValidationEvents = null) + { + var policyEventHandlerFactory = Substitute.For(); + + // Setup factory to return handlers based on type + policyEventHandlerFactory.GetHandler(Arg.Any()) + .Returns(callInfo => + { + var policyType = callInfo.Arg(); + var handler = policyDependencyEvents?.FirstOrDefault(e => e.Type == policyType); + return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); + }); + + policyEventHandlerFactory.GetHandler(Arg.Any()) + .Returns(callInfo => + { + var policyType = callInfo.Arg(); + var handler = policyValidationEvents?.FirstOrDefault(e => e.Type == policyType); + return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); + }); + + policyEventHandlerFactory.GetHandler(Arg.Any()) + .Returns(new None()); + + policyEventHandlerFactory.GetHandler(Arg.Any()) + .Returns(new None()); + + return new SutProvider() + .WithFakeTimeProvider() + .SetDependency(policyDependencyEvents ?? []) + .SetDependency(policyEventHandlerFactory) + .Create(); + } + + private static void ArrangeOrganization(SutProvider sutProvider, PolicyUpdate policyUpdate) + { + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(policyUpdate.OrganizationId) + .Returns(new OrganizationAbility + { + Id = policyUpdate.OrganizationId, + UsePolicies = true + }); + } + + private static async Task AssertPolicyNotSavedAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default!); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogPolicyEventAsync(default, default); + } + + private static async Task AssertPolicySavedAsync(SutProvider sutProvider, PolicyUpdate policyUpdate) + { + await sutProvider.GetDependency().Received(1).UpsertAsync(ExpectedPolicy()); + + await sutProvider.GetDependency().Received(1) + .LogPolicyEventAsync(ExpectedPolicy(), EventType.Policy_Updated); + + return; + + Policy ExpectedPolicy() => Arg.Is( + p => + p.Type == policyUpdate.Type + && p.OrganizationId == policyUpdate.OrganizationId + && p.Enabled == policyUpdate.Enabled + && p.Data == policyUpdate.Data); + } +} From d722314e2de45b5f1c95965d13cc500bbeb37ca9 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:15:29 -0400 Subject: [PATCH 12/52] [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 --- .gitignore | 1 + src/Core/MailTemplates/Mjml/README.md | 109 +++++++++++++-- src/Core/MailTemplates/Mjml/build.js | 128 ++++++++++++++++++ src/Core/MailTemplates/Mjml/build.sh | 4 - .../MailTemplates/Mjml/components/head.mjml | 14 +- .../MailTemplates/Mjml/emails/two-factor.mjml | 4 +- src/Core/MailTemplates/Mjml/package.json | 6 +- src/Core/MailTemplates/README.md | 78 +++++++++++ 8 files changed, 326 insertions(+), 18 deletions(-) create mode 100644 src/Core/MailTemplates/Mjml/build.js delete mode 100755 src/Core/MailTemplates/Mjml/build.sh create mode 100644 src/Core/MailTemplates/README.md diff --git a/.gitignore b/.gitignore index 3b1f40e673..059c94bac3 100644 --- a/.gitignore +++ b/.gitignore @@ -215,6 +215,7 @@ bitwarden_license/src/Sso/wwwroot/assets **/**.swp .mono src/Core/MailTemplates/Mjml/out +src/Core/MailTemplates/Mjml/out-hbs src/Admin/Admin.zip src/Api/Api.zip diff --git a/src/Core/MailTemplates/Mjml/README.md b/src/Core/MailTemplates/Mjml/README.md index b60655140a..7a497252d0 100644 --- a/src/Core/MailTemplates/Mjml/README.md +++ b/src/Core/MailTemplates/Mjml/README.md @@ -1,19 +1,112 @@ -# Email templates +# MJML email templating -This directory contains MJML templates for emails sent by the application. MJML is a markup language designed to reduce the pain of coding responsive email templates. +This directory contains MJML templates for emails. MJML is a markup language designed to reduce the pain of coding responsive email templates. Component based development features in MJML improve code quality and reusability. -## Usage +MJML stands for MailJet Markup Language. -```bash +## Implementation considerations + +These `MJML` templates are compiled into HTML which will then be further consumed by our Handlebars mail service. We can continue to use this service to assign values from our View Models. This leverages the existing infrastructure. It also means we can continue to use the double brace (`{{}}`) syntax within MJML since Handlebars can be used to assign values to those `{{variables}}`. + +There is no change on how we interact with our view models. + +There is an added step where we compile `*.mjml` to `*.html.hbs`. `*.html.hbs` is the format we use so the handlebars service can apply the variables. This build pipeline process is in progress and may need to be manually done at times. + +### `*.txt.hbs` + +There is no change to how we create the `txt.hbs`. MJML does not impact how we create these artifacts. + +## Building `MJML` files + +```shell npm ci -# Build once +# Build *.html to ./out directory npm run build -# To build on changes -npm run watch +# To build on changes to *.mjml and *.js files, new files will not be tracked, you will need to run again +npm run build:watch + +# Build *.html.hbs to ./out directory +npm run build:hbs + +# Build minified *.html.hbs to ./out directory +npm run build:minify + +# apply prettier formatting +npm run prettier ``` ## Development -MJML supports components and you can create your own components by adding them to `.mjmlconfig`. +MJML supports components and you can create your own components by adding them to `.mjmlconfig`. Components are simple JavaScript that return MJML markup based on the attributes assigned, see components/mj-bw-hero.js. The markup is not a proper object, but contained in a string. + +When using MJML templating you can use the above [commands](#building-mjml-files) to compile the template and view it in a web browser. + +Not all MJML tags have the same attributes, it is highly recommended to review the documentation on the official MJML website to understand the usages of each of the tags. + +### Recommended development + +#### Mjml email template development + +1. create `cool-email.mjml` in appropriate team directory +2. run `npm run build:watch` +3. view compiled `HTML` output in a web browser +4. iterate -> while `build:watch`'ing you should be able to refresh the browser page after the mjml/js re-compile to see the changes + +#### Testing with `IMailService` + +After the email is developed from the [initial step](#mjml-email-template-development) make sure the email `{{variables}}` are populated properly by running it through an `IMailService` implementation. + +1. run `npm run build:minify` +2. copy built `*.html.hbs` files from the build directory to a location the mail service can consume them +3. run code that will send the email + +The minified `html.hbs` artifacts are deliverables and must be placed into the correct `src/Core/MailTemplates/Handlebars/` directories in order to be used by `IMailService` implementations. + +### Custom tags + +There is currently a `mj-bw-hero` tag you can use within your `*.mjml` templates. This is a good example of how to create a component that takes in attribute values allowing us to be more DRY in our development of emails. Since the attribute's input is a string we are able to define whatever we need into the component, in this case `mj-bw-hero`. + +In order to view the custom component you have written you will need to include it in the `.mjmlconfig` and reference it in an `mjml` template file. + +```html + + +``` + +Attributes in Custom Components are defined by the developer. They can be required or optional depending on implementation. See the official MJML documentation for more information. + +```js +static allowedAttributes = { + "img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area + title: "string", // REQUIRED: large text stating primary purpose of the email + "button-text": "string", // OPTIONAL: text to display in the button + "button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked + "sub-title": "string", // OPTIONAL: smaller text providing additional context for the title +}; + +static defaultAttributes = {}; +``` + +Custom components, such as `mj-bw-hero`, must be defined in the `.mjmlconfig` in order for them to be compiled and rendered properly in the templates. + +```json +{ + "packages": ["components/mj-bw-hero"] +} +``` + +### `mj-include` + +You are also able to reference other more static MJML templates in your MJML file simply by referencing the file within the MJML template. + +```html + + + + +``` diff --git a/src/Core/MailTemplates/Mjml/build.js b/src/Core/MailTemplates/Mjml/build.js new file mode 100644 index 0000000000..db8a7fe433 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/build.js @@ -0,0 +1,128 @@ +const mjml2html = require("mjml"); +const { registerComponent } = require("mjml-core"); +const fs = require("fs"); +const path = require("path"); +const glob = require("glob"); + +// Parse command line arguments +const args = process.argv.slice(2); // Remove 'node' and script path + +// Parse flags +const flags = { + minify: args.includes("--minify") || args.includes("-m"), + watch: args.includes("--watch") || args.includes("-w"), + hbs: args.includes("--hbs") || args.includes("-h"), + trace: args.includes("--trace") || args.includes("-t"), + clean: args.includes("--clean") || args.includes("-c"), + help: args.includes("--help"), +}; + +// Use __dirname to get absolute paths relative to the script location +const config = { + inputDir: path.join(__dirname, "emails"), + outputDir: path.join(__dirname, "out"), + minify: flags.minify, + validationLevel: "strict", + hbsOutput: flags.hbs, +}; + +// Debug output +if (flags.trace) { + console.log("[DEBUG] Script location:", __dirname); + console.log("[DEBUG] Input directory:", config.inputDir); + console.log("[DEBUG] Output directory:", config.outputDir); +} + +// Ensure output directory exists +if (!fs.existsSync(config.outputDir)) { + fs.mkdirSync(config.outputDir, { recursive: true }); + if (flags.trace) { + console.log("[INFO] Created output directory:", config.outputDir); + } +} + +// Find all MJML files with absolute path +const mjmlFiles = glob.sync(`${config.inputDir}/**/*.mjml`); + +console.log(`\n[INFO] Found ${mjmlFiles.length} MJML file(s) to compile...`); + +if (mjmlFiles.length === 0) { + console.error("[ERROR] No MJML files found!"); + console.error("[ERROR] Looked in:", config.inputDir); + console.error( + "[ERROR] Does this directory exist?", + fs.existsSync(config.inputDir), + ); + process.exit(1); +} + +// Compile each MJML file +let successCount = 0; +let errorCount = 0; + +mjmlFiles.forEach((filePath) => { + try { + const mjmlContent = fs.readFileSync(filePath, "utf8"); + const fileName = path.basename(filePath, ".mjml"); + const relativePath = path.relative(config.inputDir, filePath); + + console.log(`\n[BUILD] Compiling: ${relativePath}`); + + // Compile MJML to HTML + const result = mjml2html(mjmlContent, { + minify: config.minify, + validationLevel: config.validationLevel, + filePath: filePath, // Important: tells MJML where the file is for resolving includes + mjmlConfigPath: __dirname, // Point to the directory with .mjmlconfig + }); + + // Check for errors + if (result.errors.length > 0) { + console.error(`[ERROR] Failed to compile ${fileName}.mjml:`); + result.errors.forEach((err) => + console.error(` ${err.formattedMessage}`), + ); + errorCount++; + return; + } + + // Calculate output path preserving directory structure + const relativeDir = path.dirname(relativePath); + const outputDir = path.join(config.outputDir, relativeDir); + + // Ensure subdirectory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const outputExtension = config.hbsOutput ? ".html.hbs" : ".html"; + const outputPath = path.join(outputDir, `${fileName}${outputExtension}`); + fs.writeFileSync(outputPath, result.html); + + console.log( + `[OK] Built: ${fileName}.mjml → ${path.relative(__dirname, outputPath)}`, + ); + successCount++; + + // Log warnings if any + if (result.warnings && result.warnings.length > 0) { + console.warn(`[WARN] Warnings for ${fileName}.mjml:`); + result.warnings.forEach((warn) => + console.warn(` ${warn.formattedMessage}`), + ); + } + } catch (error) { + console.error(`[ERROR] Exception processing ${path.basename(filePath)}:`); + console.error(` ${error.message}`); + errorCount++; + } +}); + +console.log(`\n[SUMMARY] Compilation complete!`); +console.log(` Success: ${successCount}`); +console.log(` Failed: ${errorCount}`); +console.log(` Output: ${config.outputDir}`); + +if (errorCount > 0) { + process.exit(1); +} diff --git a/src/Core/MailTemplates/Mjml/build.sh b/src/Core/MailTemplates/Mjml/build.sh deleted file mode 100755 index c76bdd8f61..0000000000 --- a/src/Core/MailTemplates/Mjml/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -# TODO: This should probably be replaced with a node script building every file in `emails/` - -npx mjml emails/invite.mjml -o out/invite.html -npx mjml emails/two-factor.mjml -o out/two-factor.html diff --git a/src/Core/MailTemplates/Mjml/components/head.mjml b/src/Core/MailTemplates/Mjml/components/head.mjml index 929057fb70..389ae77c12 100644 --- a/src/Core/MailTemplates/Mjml/components/head.mjml +++ b/src/Core/MailTemplates/Mjml/components/head.mjml @@ -8,9 +8,17 @@ - .link { text-decoration: none; color: #175ddc; font-weight: 600 } + .link { + text-decoration: none; + color: #175ddc; + font-weight: 600; + } - .border-fix > table { border-collapse:separate !important; } .border-fix > - table > tbody > tr > td { border-radius: 3px; } + .border-fix > table { + border-collapse: separate !important; + } + .border-fix > table > tbody > tr > td { + border-radius: 3px; + } diff --git a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml b/src/Core/MailTemplates/Mjml/emails/two-factor.mjml index b959ec1c8a..5091e208d3 100644 --- a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml +++ b/src/Core/MailTemplates/Mjml/emails/two-factor.mjml @@ -15,7 +15,9 @@ -

Your two-step verification code is: {{Token}}

+

+ Your two-step verification code is: {{ Token }} +

Use this code to complete logging in with Bitwarden.

diff --git a/src/Core/MailTemplates/Mjml/package.json b/src/Core/MailTemplates/Mjml/package.json index 8a8f81e845..f74279da7b 100644 --- a/src/Core/MailTemplates/Mjml/package.json +++ b/src/Core/MailTemplates/Mjml/package.json @@ -15,8 +15,10 @@ }, "homepage": "https://bitwarden.com", "scripts": { - "build": "./build.sh", - "watch": "nodemon --exec ./build.sh --watch ./components --watch ./emails --ext js,mjml", + "build": "node ./build.js", + "build:hbs": "node ./build.js --hbs", + "build:minify": "node ./build.js --hbs --minify", + "build:watch": "nodemon ./build.js --watch emails --watch components --ext mjml,js", "prettier": "prettier --cache --write ." }, "dependencies": { diff --git a/src/Core/MailTemplates/README.md b/src/Core/MailTemplates/README.md new file mode 100644 index 0000000000..bd42b2a10f --- /dev/null +++ b/src/Core/MailTemplates/README.md @@ -0,0 +1,78 @@ +Email templating +================ + +We use MJML to generate the HTML that our mail services use to send emails to users. To accomplish this, we use different file types depending on which part of the email generation process we're working with. + +# File Types + +## `*.html.hbs` +These are the compiled HTML email templates that serve as the foundation for all HTML emails sent by the Bitwarden platform. They are generated from MJML source files and enhanced with Handlebars templating capabilities. + +### Generation Process +- **Source**: Built from `*.mjml` files in the `./mjml` directory. + - The MJML source acts as a toolkit for developers to generate HTML. It is the developers responsibility to generate the HTML and then ensure it is accessible to `IMailService` implementations. +- **Build Tool**: Generated via node build scripts: `npm run build`. + - The build script definitions can be viewed in the `Mjml/package.json` as well as in `Mjml/build.js`. +- **Output**: Cross-client compatible HTML with embedded CSS for maximum email client support +- **Template Engine**: Enhanced with Handlebars syntax for dynamic content injection + +### Handlebars Integration +The templates use Handlebars templating syntax for dynamic content replacement: + +```html + +

Welcome {{userName}}!

+

Your organization {{organizationName}} has invited you to join.

+Accept Invitation +``` + +**Variable Types:** +- **Simple Variables**: `{{userName}}`, `{{email}}`, `{{organizationName}}` + +### Email Service Integration +The `IMailService` consumes these templates through the following process: + +1. **Template Selection**: Service selects appropriate `.html.hbs` template based on email type +2. **Model Binding**: View model properties are mapped to Handlebars variables +3. **Compilation**: Handlebars engine processes variables and generates final HTML + +### Development Guidelines + +**Variable Naming:** +- Use camelCase for consistency: `{{userName}}`, `{{organizationName}}` +- Prefix URLs with descriptive names: `{{actionUrl}}`, `{{logoUrl}}` + +**Testing Considerations:** +- Verify Handlebars variable replacement with actual view model data +- Ensure graceful degradation when variables are missing or null, if necessary +- Validate HTML structure and accessibility compliance + +## `*.txt.hbs` +These files provide plain text versions of emails and are essential for email accessibility and deliverability. They serve several important purposes: + +### Purpose and Usage +- **Accessibility**: Screen readers and assistive technologies often work better with plain text versions +- **Email Client Compatibility**: Some email clients prefer or only display plain text versions +- **Fallback Content**: When HTML rendering fails, the plain text version ensures the message is still readable + +### Structure +Plain text email templates use the same Handlebars syntax (`{{variable}}`) as HTML templates for dynamic content replacement. They should: + +- Contain the core message content without HTML formatting +- Use line breaks and spacing for readability +- Include all important links as full URLs +- Maintain logical content hierarchy using spacing and simple text formatting + +### Email Service Integration +The `IMailService` automatically uses both versions when sending emails: +- The HTML version (from `*.html.hbs`) provides rich formatting and styling +- The plain text version (from `*.txt.hbs`) serves as the text alternative +- Email clients can choose which version to display based on user preferences and capabilities + +### Development Guidelines +- Always create a corresponding `*.txt.hbs` file for each `*.html.hbs` template +- Keep the content concise but complete - include all essential information from the HTML version +- Test plain text templates to ensure they're readable and convey the same message + +## `*.mjml` +This is a templating language we use to increase efficiency when creating email content. See the readme within the `./mjml` directory for more comprehensive information. From ac20c84611686906f27cd91739f579eb7be3e5d4 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:29:47 -0700 Subject: [PATCH 13/52] [PM-24747] Remove chromium importer feature flag (#6415) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 80b74877c5..b588ad05b5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -226,7 +226,6 @@ public static class FeatureFlagKeys /* Tools Team */ public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators"; - public const string UseChromiumImporter = "pm-23982-chromium-importer"; /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; From 84534eb8f9bc445061b1fb3b1369321dd54cac93 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:14:49 +0100 Subject: [PATCH 14/52] Add the PremiumUpgradeNewDesign flag (#6438) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b588ad05b5..137bb41507 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -182,6 +182,7 @@ public static class FeatureFlagKeys public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; + public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page"; /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; From 42568b6494b902dacf766a12eba83eb33e768601 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 13 Oct 2025 08:03:57 -0500 Subject: [PATCH 15/52] [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 --- .../Vault/Controllers/CiphersController.cs | 15 +++ .../Controllers/CiphersControllerTests.cs | 112 ++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 06c88ad9bb..eb04ac1210 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -754,6 +754,11 @@ public class CiphersController : Controller } } + if (cipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move an archived item to an organization."); + } + ValidateClientVersionForFido2CredentialSupport(cipher); var original = cipher.Clone(); @@ -1263,6 +1268,11 @@ public class CiphersController : Controller _logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", cipher.Id, userId, cipher.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } + + if (cipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move archived items to an organization."); + } } var shareCiphers = new List<(CipherDetails, DateTime?)>(); @@ -1275,6 +1285,11 @@ public class CiphersController : Controller ValidateClientVersionForFido2CredentialSupport(existingCipher); + if (existingCipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cannot move archived items to an organization."); + } + shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate)); } diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 416b92f841..9f54cdbea5 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1790,6 +1790,118 @@ public class CiphersControllerTests ); } + [Theory, BitAutoData] + public async Task PutShareMany_ArchivedCipher_ThrowsBadRequestException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + request.EncryptedFor = userId; + request.OrganizationId = organizationId.ToString(); + request.ArchivedDate = DateTime.UtcNow; + var model = new CipherBulkShareRequestModel + { + Ciphers = [request], + CollectionIds = [Guid.NewGuid().ToString()] + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + + Assert.Equal("Cannot move archived items to an organization.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PutShareMany_ExistingCipherArchived_ThrowsBadRequestException( + Guid organizationId, + Guid userId, + CipherWithIdRequestModel request, + SutProvider sutProvider) + { + // Request model does not have ArchivedDate (only the existing cipher does) + request.EncryptedFor = userId; + request.OrganizationId = organizationId.ToString(); + request.ArchivedDate = null; + + var model = new CipherBulkShareRequestModel + { + Ciphers = [request], + CollectionIds = [Guid.NewGuid().ToString()] + }; + + // The existing cipher from the repository IS archived + var existingCipher = new CipherDetails + { + Id = request.Id!.Value, + UserId = userId, + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), + ArchivedDate = DateTime.UtcNow + }; + + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .GetProperUserId(default) + .ReturnsForAnyArgs(userId); + sutProvider.GetDependency() + .GetManyByUserIdAsync(userId, withOrganizations: false) + .Returns(Task.FromResult((ICollection)[existingCipher])); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShareMany(model) + ); + + Assert.Equal("Cannot move archived items to an organization.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PutShare_ArchivedCipher_ThrowsBadRequestException( + Guid cipherId, + Guid organizationId, + User user, + CipherShareRequestModel model, + SutProvider sutProvider) + { + model.Cipher.OrganizationId = organizationId.ToString(); + model.Cipher.EncryptedFor = user.Id; + + var cipher = new Cipher + { + Id = cipherId, + UserId = user.Id, + ArchivedDate = DateTime.UtcNow.AddDays(-1), + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()) + }; + + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .GetByIdAsync(cipherId) + .Returns(cipher); + sutProvider.GetDependency() + .OrganizationUser(organizationId) + .Returns(Task.FromResult(true)); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PutShare(cipherId, model) + ); + + Assert.Equal("Cannot move an archived item to an organization.", exception.Message); + } + [Theory, BitAutoData] public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException( SecretVerificationRequestModel model, From d3fbfc75f0888c112d3dbe9b12631f1b6e0d0af9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:36:04 +0200 Subject: [PATCH 16/52] [deps]: Update anchore/scan-action action to v7 (#6393) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fe82f9fbe6..cec6f7d4ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,7 +269,7 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0 + uses: anchore/scan-action@f6601287cdb1efc985d6b765bbf99cb4c0ac29d8 # v7.0.0 with: image: ${{ steps.image-tags.outputs.primary_tag }} fail-build: false From fedc6b865b359aa68598df68b928a4c06246cfc0 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:49:55 -0500 Subject: [PATCH 17/52] [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 --- .gitignore | 1 + .../OrganizationBillingController.cs | 4 +- .../OrganizationBillingVNextController.cs | 19 + .../Responses/OrganizationMetadataResponse.cs | 31 -- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Models/OrganizationMetadata.cs | 18 - .../Queries/GetOrganizationMetadataQuery.cs | 95 +++++ .../Services/OrganizationBillingService.cs | 32 +- src/Core/Constants.cs | 1 + .../OrganizationBillingControllerTests.cs | 12 +- .../GetOrganizationMetadataQueryTests.cs | 369 ++++++++++++++++++ .../OrganizationBillingServiceTests.cs | 12 +- 12 files changed, 498 insertions(+), 97 deletions(-) delete mode 100644 src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs create mode 100644 src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs create mode 100644 test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs diff --git a/.gitignore b/.gitignore index 059c94bac3..14ab10a18e 100644 --- a/.gitignore +++ b/.gitignore @@ -232,3 +232,4 @@ bitwarden_license/src/Sso/Sso.zip /identity.json /api.json /api.public.json +.serena/ diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1d6bf51661..6e4cacc155 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -38,9 +38,7 @@ public class OrganizationBillingController( return Error.NotFound(); } - var response = OrganizationMetadataResponse.From(metadata); - - return TypedResults.Ok(response); + return TypedResults.Ok(metadata); } [HttpGet("history")] diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index 2f825f2cb9..64ec068a5e 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -4,6 +4,7 @@ using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requests.Subscriptions; using Bit.Api.Billing.Models.Requirements; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Organizations.Queries; @@ -25,6 +26,7 @@ public class OrganizationBillingVNextController( ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, + IGetOrganizationMetadataQuery getOrganizationMetadataQuery, IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IRestartSubscriptionCommand restartSubscriptionCommand, @@ -113,6 +115,23 @@ public class OrganizationBillingVNextController( return Handle(result); } + [Authorize] + [HttpGet("metadata")] + [RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)] + [InjectOrganization] + public async Task GetMetadataAsync( + [BindNever] Organization organization) + { + var metadata = await getOrganizationMetadataQuery.Run(organization); + + if (metadata == null) + { + return TypedResults.NotFound(); + } + + return TypedResults.Ok(metadata); + } + [Authorize] [HttpGet("warnings")] [InjectOrganization] diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs deleted file mode 100644 index a13f267c3b..0000000000 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.Billing.Organizations.Models; - -namespace Bit.Api.Billing.Models.Responses; - -public record OrganizationMetadataResponse( - bool IsEligibleForSelfHost, - bool IsManaged, - bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid, - bool HasSubscription, - bool HasOpenInvoice, - bool IsSubscriptionCanceled, - DateTime? InvoiceDueDate, - DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate, - int OrganizationOccupiedSeats) -{ - public static OrganizationMetadataResponse From(OrganizationMetadata metadata) - => new( - metadata.IsEligibleForSelfHost, - metadata.IsManaged, - metadata.IsOnSecretsManagerStandalone, - metadata.IsSubscriptionUnpaid, - metadata.HasSubscription, - metadata.HasOpenInvoice, - metadata.IsSubscriptionCanceled, - metadata.InvoiceDueDate, - metadata.InvoiceCreatedDate, - metadata.SubPeriodEndDate, - metadata.OrganizationOccupiedSeats); -} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 7aec422a4b..d6593f5365 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ public static class ServiceCollectionExtensions services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); services.AddPremiumCommands(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs index 2bcd213dbf..fedd0ad78c 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs @@ -1,28 +1,10 @@ namespace Bit.Core.Billing.Organizations.Models; public record OrganizationMetadata( - bool IsEligibleForSelfHost, - bool IsManaged, bool IsOnSecretsManagerStandalone, - bool IsSubscriptionUnpaid, - bool HasSubscription, - bool HasOpenInvoice, - bool IsSubscriptionCanceled, - DateTime? InvoiceDueDate, - DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate, int OrganizationOccupiedSeats) { public static OrganizationMetadata Default => new OrganizationMetadata( false, - false, - false, - false, - false, - false, - false, - null, - null, - null, 0); } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs new file mode 100644 index 0000000000..63da0477a1 --- /dev/null +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationMetadataQuery.cs @@ -0,0 +1,95 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Stripe; + +namespace Bit.Core.Billing.Organizations.Queries; + +public interface IGetOrganizationMetadataQuery +{ + Task Run(Organization organization); +} + +public class GetOrganizationMetadataQuery( + IGlobalSettings globalSettings, + IOrganizationRepository organizationRepository, + IPricingClient pricingClient, + ISubscriberService subscriberService) : IGetOrganizationMetadataQuery +{ + public async Task Run(Organization organization) + { + if (organization == null) + { + return null; + } + + if (globalSettings.SelfHosted) + { + return OrganizationMetadata.Default; + } + + var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + return OrganizationMetadata.Default with + { + OrganizationOccupiedSeats = orgOccupiedSeats.Total + }; + } + + var customer = await subscriberService.GetCustomer(organization, + new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); + + var subscription = await subscriberService.GetSubscription(organization); + + if (customer == null || subscription == null) + { + return OrganizationMetadata.Default with + { + OrganizationOccupiedSeats = orgOccupiedSeats.Total + }; + } + + var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); + + return new OrganizationMetadata( + isOnSecretsManagerStandalone, + orgOccupiedSeats.Total); + } + + private async Task IsOnSecretsManagerStandalone( + Organization organization, + Customer? customer, + Subscription? subscription) + { + if (customer == null || subscription == null) + { + return false; + } + + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + if (!plan.SupportsSecretsManager) + { + return false; + } + + var hasCoupon = customer.Discount?.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone; + + if (!hasCoupon) + { + return false; + } + + var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId); + + var couponAppliesTo = customer.Discount?.Coupon?.AppliesTo?.Products; + + return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); + } +} diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 36a618f799..494a202c4b 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -74,16 +74,12 @@ public class OrganizationBillingService( return OrganizationMetadata.Default; } - var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); - - var isManaged = organization.Status == OrganizationStatusType.Managed; var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { return OrganizationMetadata.Default with { - IsEligibleForSelfHost = isEligibleForSelfHost, - IsManaged = isManaged, OrganizationOccupiedSeats = orgOccupiedSeats.Total }; } @@ -97,28 +93,14 @@ public class OrganizationBillingService( { return OrganizationMetadata.Default with { - IsEligibleForSelfHost = isEligibleForSelfHost, - IsManaged = isManaged + OrganizationOccupiedSeats = orgOccupiedSeats.Total }; } var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription); - var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId) - ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) - : null; - return new OrganizationMetadata( - isEligibleForSelfHost, - isManaged, isOnSecretsManagerStandalone, - subscription.Status == StripeConstants.SubscriptionStatus.Unpaid, - true, - invoice?.Status == StripeConstants.InvoiceStatus.Open, - subscription.Status == StripeConstants.SubscriptionStatus.Canceled, - invoice?.DueDate, - invoice?.Created, - subscription.CurrentPeriodEnd, orgOccupiedSeats.Total); } @@ -536,16 +518,6 @@ public class OrganizationBillingService( return customer; } - private async Task IsEligibleForSelfHostAsync( - Organization organization) - { - var plans = await pricingClient.ListPlans(); - - var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type); - - return eligibleSelfHostPlans.Contains(organization.PlanType); - } - private async Task IsOnSecretsManagerStandalone( Organization organization, Customer? customer, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 137bb41507..f4a3c668ff 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -179,6 +179,7 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; + public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure"; public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog"; public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 51866320ee..d79bfde893 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -1,5 +1,4 @@ using Bit.Api.Billing.Controllers; -using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; @@ -53,19 +52,16 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0)); + .Returns(new OrganizationMetadata(true, 10)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); - Assert.IsType>(result); + Assert.IsType>(result); - var response = ((Ok)result).Value; + var response = ((Ok)result).Value; - Assert.True(response.IsEligibleForSelfHost); - Assert.True(response.IsManaged); Assert.True(response.IsOnSecretsManagerStandalone); - Assert.True(response.IsSubscriptionUnpaid); - Assert.True(response.HasSubscription); + Assert.Equal(10, response.OrganizationOccupiedSeats); } [Theory, BitAutoData] diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs new file mode 100644 index 0000000000..21081112d7 --- /dev/null +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationMetadataQueryTests.cs @@ -0,0 +1,369 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Organizations.Queries; + +[SutProviderCustomize] +public class GetOrganizationMetadataQueryTests +{ + [Theory, BitAutoData] + public async Task Run_NullOrganization_ReturnsNull( + SutProvider sutProvider) + { + var result = await sutProvider.Sut.Run(null); + + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task Run_SelfHosted_ReturnsDefault( + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency().SelfHosted.Returns(true); + + var result = await sutProvider.Sut.Run(organization); + + Assert.Equal(OrganizationMetadata.Default, result); + } + + [Theory, BitAutoData] + public async Task Run_NoGatewaySubscriptionId_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = null; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 10, Sponsored = 0 }); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(10, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_NullCustomer_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .ReturnsNull(); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(5, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_NullSubscription_ReturnsDefaultWithOccupiedSeats( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + + var customer = new Customer(); + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .ReturnsNull(); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(7, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_WithSecretsManagerStandaloneCoupon_ReturnsMetadataWithFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.EnterpriseAnnually; + + var productId = "product_123"; + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = [productId] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = productId + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.True(result.IsOnSecretsManagerStandalone); + Assert.Equal(15, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_WithoutSecretsManagerStandaloneCoupon_ReturnsMetadataWithoutFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.TeamsAnnually; + + var customer = new Customer + { + Discount = null + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = "product_123" + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(20, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_CouponDoesNotApplyToSubscriptionProducts_ReturnsFalseForStandaloneFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.EnterpriseAnnually; + + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = ["different_product_id"] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = "product_123" + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(12, result.OrganizationOccupiedSeats); + } + + [Theory, BitAutoData] + public async Task Run_PlanDoesNotSupportSecretsManager_ReturnsFalseForStandaloneFlag( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = "sub_123"; + organization.PlanType = PlanType.FamiliesAnnually; + + var productId = "product_123"; + var customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon + { + Id = StripeConstants.CouponIDs.SecretsManagerStandalone, + AppliesTo = new CouponAppliesTo + { + Products = [productId] + } + } + } + }; + + var subscription = new Subscription + { + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + Plan = new Plan + { + ProductId = productId + } + } + ] + } + }; + + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 }); + + sutProvider.GetDependency() + .GetCustomer(organization, Arg.Is(options => + options.Expand.Contains("discount.coupon.applies_to"))) + .Returns(customer); + + sutProvider.GetDependency() + .GetSubscription(organization) + .Returns(subscription); + + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); + + var result = await sutProvider.Sut.Run(organization); + + Assert.NotNull(result); + Assert.False(result.IsOnSecretsManagerStandalone); + Assert.Equal(8, result.OrganizationOccupiedSeats); + } +} diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 7edc60a26a..77dce8101c 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -96,6 +96,10 @@ public class OrganizationBillingServiceTests sutProvider.GetDependency().GetPlanOrThrow(organization.PlanType) .Returns(StaticStore.GetPlan(organization.PlanType)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 }); + var subscriberService = sutProvider.GetDependency(); // Set up subscriber service to return null for customer @@ -110,13 +114,7 @@ public class OrganizationBillingServiceTests Assert.NotNull(metadata); Assert.False(metadata!.IsOnSecretsManagerStandalone); - Assert.False(metadata.HasSubscription); - Assert.False(metadata.IsSubscriptionUnpaid); - Assert.False(metadata.HasOpenInvoice); - Assert.False(metadata.IsSubscriptionCanceled); - Assert.Null(metadata.InvoiceDueDate); - Assert.Null(metadata.InvoiceCreatedDate); - Assert.Null(metadata.SubPeriodEndDate); + Assert.Equal(1, metadata.OrganizationOccupiedSeats); } #endregion From 2d41e64a8b79ab3b484e5fd0c2c52c9de11000f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:39:57 +0200 Subject: [PATCH 18/52] [deps]: Update actions/setup-dotnet action to v5 (#6333) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 8 ++++---- .github/workflows/test-database.yml | 4 ++-- .github/workflows/test.yml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec6f7d4ae..907f50197b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Verify format run: dotnet format --verify-no-changes @@ -117,7 +117,7 @@ jobs: fi - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Set up Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 @@ -299,7 +299,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -425,7 +425,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Print environment run: | diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 6bbc33299f..cdba344195 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Restore tools run: dotnet tool restore @@ -179,7 +179,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Print environment run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4eed6df7ab..d584d6c0af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 - name: Print environment run: | From c2302c0dad25f39b07ed734900348ad02962883a Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Tue, 14 Oct 2025 17:41:27 +0200 Subject: [PATCH 19/52] Add feature flag (#6399) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f4a3c668ff..42ddd33c37 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -197,6 +197,7 @@ public static class FeatureFlagKeys public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings"; public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data"; public const string WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2"; + public const string LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2"; public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change"; /* Mobile Team */ From f743d5e853c76fa96c7e3c7b6a5ef61fa2951fc8 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:11:19 -0500 Subject: [PATCH 20/52] Save organization's customer ID before invoking hasPaymentMethodQuery (#6442) --- .../Services/OrganizationBillingService.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 494a202c4b..9af9cdb345 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -51,13 +51,8 @@ public class OrganizationBillingService( { organization.Enabled = true; organization.ExpirationDate = subscription.CurrentPeriodEnd; + await organizationRepository.ReplaceAsync(organization); } - - organization.Gateway = GatewayType.Stripe; - organization.GatewayCustomerId = customer.Id; - organization.GatewaySubscriptionId = subscription.Id; - - await organizationRepository.ReplaceAsync(organization); } public async Task GetMetadata(Guid organizationId) @@ -255,8 +250,6 @@ public class OrganizationBillingService( ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - - if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && customerSetup.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates) { @@ -335,7 +328,13 @@ public class OrganizationBillingService( try { - return await stripeAdapter.CustomerCreateAsync(customerCreateOptions); + var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions); + + organization.Gateway = GatewayType.Stripe; + organization.GatewayCustomerId = customer.Id; + await organizationRepository.ReplaceAsync(organization); + + return customer; } catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) @@ -476,7 +475,13 @@ public class OrganizationBillingService( { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); + + var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); + + organization.GatewaySubscriptionId = subscription.Id; + await organizationRepository.ReplaceAsync(organization); + + return subscription; } private async Task GetCustomerWhileEnsuringCorrectTaxExemptionAsync( From 58e9b4aa9ccb22387e1ed028be849c3b8a7f1809 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 14 Oct 2025 13:42:29 -0500 Subject: [PATCH 21/52] Remove unused FF (#6455) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 42ddd33c37..a4ab44cc8d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -172,7 +172,6 @@ public static class FeatureFlagKeys public const string WindowsDesktopAutotype = "windows-desktop-autotype"; /* Billing Team */ - public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email"; public const string TrialPayment = "PM-8163-trial-payment"; public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; From 018ecc05ed1b0865bceec826a9312ea2ce156caa Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:43:18 -0700 Subject: [PATCH 22/52] remove feature flag (#6419) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a4ab44cc8d..c644c61d1c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -233,7 +233,6 @@ public static class FeatureFlagKeys public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string CipherKeyEncryption = "cipher-key-encryption"; - public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; public const string PhishingDetection = "phishing-detection"; From 0fb70996209e5007cffe19f119c56a9324e00418 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 15 Oct 2025 14:36:51 +0200 Subject: [PATCH 23/52] Add feature flag (#6453) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c644c61d1c..d0d63ff787 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -198,6 +198,7 @@ public static class FeatureFlagKeys public const string WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2"; public const string LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2"; public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change"; + public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption"; /* Mobile Team */ public const string NativeCarouselFlow = "native-carousel-flow"; From 132db95fb76f86705b203b0b771ccedd383c0f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:18:37 +0100 Subject: [PATCH 24/52] [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 --- .../Implementations/VNextSavePolicyCommand.cs | 77 ++++---- .../PolicyServiceCollectionExtensions.cs | 14 ++ ...reeFamiliesForEnterprisePolicyValidator.cs | 8 +- .../MaximumVaultTimeoutPolicyValidator.cs | 3 +- ...rganizationDataOwnershipPolicyValidator.cs | 19 +- .../RequireSsoPolicyValidator.cs | 8 +- .../ResetPasswordPolicyValidator.cs | 8 +- .../SingleOrgPolicyValidator.cs | 13 +- .../TwoFactorAuthenticationPolicyValidator.cs | 8 +- ...miliesForEnterprisePolicyValidatorTests.cs | 61 +++++++ ...zationDataOwnershipPolicyValidatorTests.cs | 172 ++++++++++++++++++ .../RequireSsoPolicyValidatorTests.cs | 62 +++++++ .../ResetPasswordPolicyValidatorTests.cs | 55 ++++++ .../SingleOrgPolicyValidatorTests.cs | 132 ++++++++++++++ ...actorAuthenticationPolicyValidatorTests.cs | 120 ++++++++++++ .../Policies/VNextSavePolicyCommandTests.cs | 50 ++--- 16 files changed, 721 insertions(+), 89 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs index 1a2b78fc8a..5d40cb211f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/VNextSavePolicyCommand.cs @@ -13,25 +13,11 @@ public class VNextSavePolicyCommand( IApplicationCacheService applicationCacheService, IEventService eventService, IPolicyRepository policyRepository, - IEnumerable policyValidationEventHandlers, + IEnumerable policyUpdateEventHandlers, TimeProvider timeProvider, IPolicyEventHandlerFactory policyEventHandlerFactory) : IVNextSavePolicyCommand { - private readonly IReadOnlyDictionary _policyValidationEvents = MapToDictionary(policyValidationEventHandlers); - - private static Dictionary MapToDictionary(IEnumerable policyValidationEventHandlers) - { - var policyValidationEventsDict = new Dictionary(); - foreach (var policyValidationEvent in policyValidationEventHandlers) - { - if (!policyValidationEventsDict.TryAdd(policyValidationEvent.Type, policyValidationEvent)) - { - throw new Exception($"Duplicate PolicyValidationEvent for {policyValidationEvent.Type} policy."); - } - } - return policyValidationEventsDict; - } public async Task SaveAsync(SavePolicyModel policyRequest) { @@ -112,32 +98,26 @@ public class VNextSavePolicyCommand( Policy? currentPolicy, Dictionary savedPoliciesDict) { - var result = policyEventHandlerFactory.GetHandler(policyUpdateRequest.Type); + var isCurrentlyEnabled = currentPolicy?.Enabled == true; + var isBeingEnabled = policyUpdateRequest.Enabled && !isCurrentlyEnabled; + var isBeingDisabled = !policyUpdateRequest.Enabled && isCurrentlyEnabled; - result.Switch( - validator => - { - var isCurrentlyEnabled = currentPolicy?.Enabled == true; - - switch (policyUpdateRequest.Enabled) - { - case true when !isCurrentlyEnabled: - ValidateEnablingRequirements(validator, savedPoliciesDict); - return; - case false when isCurrentlyEnabled: - ValidateDisablingRequirements(validator, policyUpdateRequest.Type, savedPoliciesDict); - break; - } - }, - _ => { }); + if (isBeingEnabled) + { + ValidateEnablingRequirements(policyUpdateRequest.Type, savedPoliciesDict); + } + else if (isBeingDisabled) + { + ValidateDisablingRequirements(policyUpdateRequest.Type, savedPoliciesDict); + } } private void ValidateDisablingRequirements( - IEnforceDependentPoliciesEvent validator, PolicyType policyType, Dictionary savedPoliciesDict) { - var dependentPolicyTypes = _policyValidationEvents.Values + var dependentPolicyTypes = policyUpdateEventHandlers + .OfType() .Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyType)) .Select(otherValidator => otherValidator.Type) .Where(otherPolicyType => savedPoliciesDict.TryGetValue(otherPolicyType, out var savedPolicy) && @@ -147,24 +127,31 @@ public class VNextSavePolicyCommand( switch (dependentPolicyTypes) { case { Count: 1 }: - throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy."); + throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {policyType.GetName()} policy."); case { Count: > 1 }: - throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy."); + throw new BadRequestException($"Turn off all of the policies that require the {policyType.GetName()} policy."); } } - private static void ValidateEnablingRequirements( - IEnforceDependentPoliciesEvent validator, + private void ValidateEnablingRequirements( + PolicyType policyType, Dictionary savedPoliciesDict) { - var missingRequiredPolicyTypes = validator.RequiredPolicies - .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) - .ToList(); + var result = policyEventHandlerFactory.GetHandler(policyType); - if (missingRequiredPolicyTypes.Count != 0) - { - throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy."); - } + result.Switch( + validator => + { + var missingRequiredPolicyTypes = validator.RequiredPolicies + .Where(requiredPolicyType => savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true }) + .ToList(); + + if (missingRequiredPolicyTypes.Count != 0) + { + throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {policyType.GetName()} policy."); + } + }, + _ => { /* Policy has no required dependencies */ }); } private async Task ExecutePreUpsertSideEffectAsync( diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index f35ff87424..c90a1512a2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -22,8 +22,10 @@ public static class PolicyServiceCollectionExtensions services.AddPolicyValidators(); services.AddPolicyRequirements(); services.AddPolicySideEffects(); + services.AddPolicyUpdateEvents(); } + [Obsolete("Use AddPolicyUpdateEvents instead.")] private static void AddPolicyValidators(this IServiceCollection services) { services.AddScoped(); @@ -34,11 +36,23 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); } + [Obsolete("Use AddPolicyUpdateEvents instead.")] private static void AddPolicySideEffects(this IServiceCollection services) { services.AddScoped(); } + private static void AddPolicyUpdateEvents(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + private static void AddPolicyRequirements(this IServiceCollection services) { services.AddScoped, DisableSendPolicyRequirementFactory>(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidator.cs index 57db4962e3..52a7e3e880 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidator.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -12,11 +13,16 @@ public class FreeFamiliesForEnterprisePolicyValidator( IOrganizationSponsorshipRepository organizationSponsorshipRepository, IMailService mailService, IOrganizationRepository organizationRepository) - : IPolicyValidator + : IPolicyValidator, IOnPolicyPreUpdateEvent { public PolicyType Type => PolicyType.FreeFamiliesSponsorshipPolicy; public IEnumerable RequiredPolicies => []; + public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy); + } + public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs index bfd4dcfe0d..796ed286d8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/MaximumVaultTimeoutPolicyValidator.cs @@ -3,10 +3,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator +public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator, IEnforceDependentPoliciesEvent { public PolicyType Type => PolicyType.MaximumVaultTimeout; public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs index f4ef6021a7..0bee2a55af 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs @@ -1,24 +1,32 @@  using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Repositories; using Bit.Core.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -/// -/// Please do not extend or expand this validator. We're currently in the process of refactoring our policy validator pattern. -/// This is a stop-gap solution for post-policy-save side effects, but it is not the long-term solution. -/// public class OrganizationDataOwnershipPolicyValidator( IPolicyRepository policyRepository, ICollectionRepository collectionRepository, IEnumerable> factories, IFeatureService featureService) - : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect + : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect, IOnPolicyPostUpdateEvent { + public PolicyType Type => PolicyType.OrganizationDataOwnership; + + public async Task ExecutePostUpsertSideEffectAsync( + SavePolicyModel policyRequest, + Policy postUpsertedPolicyState, + Policy? previousPolicyState) + { + await ExecuteSideEffectsAsync(policyRequest, postUpsertedPolicyState, previousPolicyState); + } + public async Task ExecuteSideEffectsAsync( SavePolicyModel policyRequest, Policy postUpdatedPolicy, @@ -68,5 +76,4 @@ public class OrganizationDataOwnershipPolicyValidator( userOrgIds, defaultCollectionName); } - } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs index 2082d4305f..adc2a3865a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidator.cs @@ -3,12 +3,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public class RequireSsoPolicyValidator : IPolicyValidator +public class RequireSsoPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent { private readonly ISsoConfigRepository _ssoConfigRepository; @@ -20,6 +21,11 @@ public class RequireSsoPolicyValidator : IPolicyValidator public PolicyType Type => PolicyType.RequireSso; public IEnumerable RequiredPolicies => [PolicyType.SingleOrg]; + public async Task ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy); + } + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (policyUpdate is not { Enabled: true }) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs index 1126c4b922..9033a38ad0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidator.cs @@ -4,12 +4,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public class ResetPasswordPolicyValidator : IPolicyValidator +public class ResetPasswordPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IEnforceDependentPoliciesEvent { private readonly ISsoConfigRepository _ssoConfigRepository; public PolicyType Type => PolicyType.ResetPassword; @@ -20,6 +21,11 @@ public class ResetPasswordPolicyValidator : IPolicyValidator _ssoConfigRepository = ssoConfigRepository; } + public async Task ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy); + } + public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (policyUpdate is not { Enabled: true } || diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index 49467eaae4..c0378bf5f9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Context; @@ -17,7 +18,7 @@ using Bit.Core.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public class SingleOrgPolicyValidator : IPolicyValidator +public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent, IOnPolicyPreUpdateEvent { public PolicyType Type => PolicyType.SingleOrg; private const string OrganizationNotFoundErrorMessage = "Organization not found."; @@ -57,6 +58,16 @@ public class SingleOrgPolicyValidator : IPolicyValidator public IEnumerable RequiredPolicies => []; + public async Task ValidateAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + return await ValidateAsync(policyRequest.PolicyUpdate, currentPolicy); + } + + public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy); + } + public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index 5ce72df6c1..7f3ebcccfb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; @@ -16,7 +17,7 @@ using Bit.Core.Services; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator +public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator, IOnPolicyPreUpdateEvent { private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; @@ -46,6 +47,11 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } + public async Task ExecutePreUpsertSideEffectAsync(SavePolicyModel policyRequest, Policy? currentPolicy) + { + await OnSaveSideEffectsAsync(policyRequest.PolicyUpdate, currentPolicy); + } + public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidatorTests.cs index 0aa670297b..8f8fd939fe 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/FreeFamiliesForEnterprisePolicyValidatorTests.cs @@ -72,4 +72,65 @@ public class FreeFamiliesForEnterprisePolicyValidatorTests organizationSponsorships[0].SponsoredOrganizationId.ToString(), organization.Name); } + + [Theory, BitAutoData] + public async Task ExecutePreUpsertSideEffectAsync_DoesNotNotifyUserWhenPolicyDisabled( + Organization organization, + List organizationSponsorships, + [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate, + [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, true)] Policy policy, + SutProvider sutProvider) + { + policy.Enabled = true; + policyUpdate.Enabled = false; + + sutProvider.GetDependency() + .GetByIdAsync(policyUpdate.OrganizationId) + .Returns(organization); + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId) + .Returns(organizationSponsorships); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync(default, default, default, default); + } + + [Theory, BitAutoData] + public async Task ExecutePreUpsertSideEffectAsync_DoesNotifyUserWhenPolicyEnabled( + Organization organization, + List organizationSponsorships, + [PolicyUpdate(PolicyType.FreeFamiliesSponsorshipPolicy)] PolicyUpdate policyUpdate, + [Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] Policy policy, + SutProvider sutProvider) + { + policy.Enabled = false; + policyUpdate.Enabled = true; + + sutProvider.GetDependency() + .GetByIdAsync(policyUpdate.OrganizationId) + .Returns(organization); + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(policyUpdate.OrganizationId) + .Returns(organizationSponsorships); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy); + + var offerAcceptanceDate = organizationSponsorships[0].ValidUntil!.Value.AddDays(-7).ToString("MM/dd/yyyy"); + await sutProvider.GetDependency() + .Received(1) + .SendFamiliesForEnterpriseRemoveSponsorshipsEmailAsync( + organizationSponsorships[0].FriendlyName, + offerAcceptanceDate, + organizationSponsorships[0].SponsoredOrganizationId.ToString(), + organization.Name); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs index a39382382b..a65290e6a7 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs @@ -274,4 +274,176 @@ public class OrganizationDataOwnershipPolicyValidatorTests return sut; } + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_FeatureFlagDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(false); + + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + + // Act + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertDefaultCollectionsAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_PolicyAlreadyEnabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState, + SutProvider sutProvider) + { + // Arrange + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + + // Act + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertDefaultCollectionsAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_PolicyBeingDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState, + SutProvider sutProvider) + { + // Arrange + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + + // Act + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertDefaultCollectionsAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ExecutePostUpsertSideEffectAsync_WhenNoUsersExist_DoNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, + OrganizationDataOwnershipPolicyRequirementFactory factory) + { + // Arrange + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + + var policyRepository = ArrangePolicyRepository([]); + var collectionRepository = Substitute.For(); + + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + + // Act + await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await collectionRepository + .DidNotReceiveWithAnyArgs() + .UpsertDefaultCollectionsAsync( + default, + default, + default); + + await policyRepository + .Received(1) + .GetPolicyDetailsByOrganizationIdAsync( + policyUpdate.OrganizationId, + PolicyType.OrganizationDataOwnership); + } + + [Theory] + [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))] + public async Task ExecutePostUpsertSideEffectAsync_WithRequirements_ShouldUpsertDefaultCollections( + Policy postUpdatedPolicy, + Policy? previousPolicyState, + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable orgPolicyDetails, + OrganizationDataOwnershipPolicyRequirementFactory factory) + { + // Arrange + var orgPolicyDetailsList = orgPolicyDetails.ToList(); + foreach (var policyDetail in orgPolicyDetailsList) + { + policyDetail.OrganizationId = policyUpdate.OrganizationId; + } + + var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList); + var collectionRepository = Substitute.For(); + + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + + // Act + await sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await collectionRepository + .Received(1) + .UpsertDefaultCollectionsAsync( + policyUpdate.OrganizationId, + Arg.Is>(ids => ids.Count() == 3), + _defaultUserCollectionName); + } + + [Theory] + [BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))] + public async Task ExecutePostUpsertSideEffectAsync_WhenDefaultCollectionNameIsInvalid_DoesNothing( + IPolicyMetadataModel metadata, + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, + SutProvider sutProvider) + { + // Arrange + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; + policyUpdate.Enabled = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + var policyRequest = new SavePolicyModel(policyUpdate, null, metadata); + + // Act + await sutProvider.Sut.ExecutePostUpsertSideEffectAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertDefaultCollectionsAsync(default, default, default); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs index d3af765f79..857aa5e09e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/RequireSsoPolicyValidatorTests.cs @@ -72,4 +72,66 @@ public class RequireSsoPolicyValidatorTests var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); Assert.True(string.IsNullOrEmpty(result)); } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError( + [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.RequireSso)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError( + [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.RequireSso)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.Contains("Trusted device encryption is on", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_DecryptionOptionsNotEnabled_Success( + [PolicyUpdate(PolicyType.RequireSso, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.RequireSso)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.True(string.IsNullOrEmpty(result)); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs index 83939406b5..cdfd549454 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/ResetPasswordPolicyValidatorTests.cs @@ -68,4 +68,59 @@ public class ResetPasswordPolicyValidatorTests var result = await sutProvider.Sut.ValidateAsync(policyUpdate, policy); Assert.True(string.IsNullOrEmpty(result)); } + + [Theory] + [BitAutoData(true, false)] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeEnabled_ValidationError( + bool policyEnabled, + bool autoEnrollEnabled, + [PolicyUpdate(PolicyType.ResetPassword)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policyUpdate.Enabled = policyEnabled; + policyUpdate.SetDataModel(new ResetPasswordDataModel + { + AutoEnrollEnabled = autoEnrollEnabled + }); + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.Contains("Trusted device encryption is on and requires this policy.", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_TdeNotEnabled_Success( + [PolicyUpdate(PolicyType.ResetPassword, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.ResetPassword)] Policy policy, + SutProvider sutProvider) + { + policyUpdate.SetDataModel(new ResetPasswordDataModel + { + AutoEnrollEnabled = false + }); + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.True(string.IsNullOrEmpty(result)); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index e982a67e46..cea464c155 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; @@ -145,4 +146,135 @@ public class SingleOrgPolicyValidatorTests .Received(1) .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorEnabled_ValidationError( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = true }; + ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector }); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.Contains("Key Connector is enabled", result, StringComparison.OrdinalIgnoreCase); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_WithSavePolicyModel_DisablingPolicy_KeyConnectorNotEnabled_Success( + [PolicyUpdate(PolicyType.SingleOrg, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = policyUpdate.OrganizationId; + + var ssoConfig = new SsoConfig { Enabled = false }; + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns(ssoConfig); + + sutProvider.GetDependency() + .HasVerifiedDomainsAsync(policyUpdate.OrganizationId) + .Returns(false); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var result = await sutProvider.Sut.ValidateAsync(savePolicyModel, policy); + Assert.True(string.IsNullOrEmpty(result)); + } + + [Theory, BitAutoData] + public async Task ExecutePreUpsertSideEffectAsync_RevokesNonCompliantUsers( + [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, + [Policy(PolicyType.SingleOrg, false)] Policy policy, + Guid savingUserId, + Guid nonCompliantUserId, + Organization organization, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + + var compliantUser1 = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user1@example.com" + }; + + var compliantUser2 = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = new Guid(), + Email = "user2@example.com" + }; + + var nonCompliantUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + Type = OrganizationUserType.User, + Status = OrganizationUserStatusType.Confirmed, + UserId = nonCompliantUserId, + Email = "user3@example.com" + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([compliantUser1, compliantUser2, nonCompliantUser]); + + var otherOrganizationUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = new Guid(), + UserId = nonCompliantUserId, + Status = OrganizationUserStatusType.Confirmed + }; + + sutProvider.GetDependency() + .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) + .Returns([otherOrganizationUser]); + + sutProvider.GetDependency().UserId.Returns(savingUserId); + sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy); + + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => + r.OrganizationId == organization.Id && + r.OrganizationUsers.Count() == 1 && + r.OrganizationUsers.First().Id == nonCompliantUser.Id)); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email); + await sutProvider.GetDependency() + .DidNotReceive() + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 7b344d3b29..9eadbcc3b8 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -136,4 +136,124 @@ public class TwoFactorAuthenticationPolicyValidatorTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), compliantUser.Email); } + + [Theory, BitAutoData] + public async Task ExecutePreUpsertSideEffectAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = false + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([orgUserDetailUserWithout2Fa]); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (orgUserDetailUserWithout2Fa, false), + }); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy)); + + Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, exception.Message); + } + + [Theory, BitAutoData] + public async Task ExecutePreUpsertSideEffectAsync_RevokesOnlyNonCompliantUsers( + Organization organization, + [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, + [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, + SutProvider sutProvider) + { + // Arrange + policy.OrganizationId = policyUpdate.OrganizationId; + organization.Id = policyUpdate.OrganizationId; + + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + + var nonCompliantUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = "user3@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + + var compliantUser = new OrganizationUserUserDetails + { + Id = Guid.NewGuid(), + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = "user4@test.com", + Name = "TEST", + UserId = Guid.NewGuid(), + HasMasterPassword = true + }; + + sutProvider.GetDependency() + .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) + .Returns([nonCompliantUser, compliantUser]); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Any>()) + .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() + { + (nonCompliantUser, false), + (compliantUser, true) + }); + + sutProvider.GetDependency() + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) + .Returns(new CommandResult()); + + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + // Act + await sutProvider.Sut.ExecutePreUpsertSideEffectAsync(savePolicyModel, policy); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); + + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync(Arg.Is(req => + req.OrganizationId == policyUpdate.OrganizationId && + req.OrganizationUsers.SequenceEqual(new[] { nonCompliantUser }) + )); + + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), + nonCompliantUser.Email); + + // Did not send out an email for compliantUser + await sutProvider.GetDependency() + .Received(0) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), + compliantUser.Email); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs index 1510042446..da10ea300f 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/VNextSavePolicyCommandTests.cs @@ -28,9 +28,10 @@ public class VNextSavePolicyCommandTests // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); - var sutProvider = SutProviderFactory( - [new FakeSingleOrgDependencyEvent()], - [fakePolicyValidationEvent]); + var sutProvider = SutProviderFactory([ + new FakeSingleOrgDependencyEvent(), + fakePolicyValidationEvent + ]); var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); @@ -71,9 +72,10 @@ public class VNextSavePolicyCommandTests // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns(""); - var sutProvider = SutProviderFactory( - [new FakeSingleOrgDependencyEvent()], - [fakePolicyValidationEvent]); + var sutProvider = SutProviderFactory([ + new FakeSingleOrgDependencyEvent(), + fakePolicyValidationEvent + ]); var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); @@ -110,23 +112,6 @@ public class VNextSavePolicyCommandTests p.RevisionDate == revisionDate)); } - [Fact] - public void Constructor_DuplicatePolicyDependencyEvents_Throws() - { - // Arrange & Act - var exception = Assert.Throws(() => - new VNextSavePolicyCommand( - Substitute.For(), - Substitute.For(), - Substitute.For(), - [new FakeSingleOrgDependencyEvent(), new FakeSingleOrgDependencyEvent()], - Substitute.For(), - Substitute.For())); - - // Assert - Assert.Contains("Duplicate PolicyValidationEvent for SingleOrg policy", exception.Message); - } - [Theory, BitAutoData] public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyUpdate(PolicyType.ActivateAutofill)] PolicyUpdate policyUpdate) { @@ -366,9 +351,10 @@ public class VNextSavePolicyCommandTests // Arrange var fakePolicyValidationEvent = new FakeSingleOrgValidationEvent(); fakePolicyValidationEvent.ValidateAsyncMock(Arg.Any(), Arg.Any()).Returns("Validation error!"); - var sutProvider = SutProviderFactory( - [new FakeSingleOrgDependencyEvent()], - [fakePolicyValidationEvent]); + var sutProvider = SutProviderFactory([ + new FakeSingleOrgDependencyEvent(), + fakePolicyValidationEvent + ]); var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); @@ -392,20 +378,20 @@ public class VNextSavePolicyCommandTests } /// - /// Returns a new SutProvider with the PolicyDependencyEvents registered in the Sut. + /// Returns a new SutProvider with the PolicyUpdateEvents registered in the Sut. /// private static SutProvider SutProviderFactory( - IEnumerable? policyDependencyEvents = null, - IEnumerable? policyValidationEvents = null) + IEnumerable? policyUpdateEvents = null) { var policyEventHandlerFactory = Substitute.For(); + var handlers = policyUpdateEvents ?? []; // Setup factory to return handlers based on type policyEventHandlerFactory.GetHandler(Arg.Any()) .Returns(callInfo => { var policyType = callInfo.Arg(); - var handler = policyDependencyEvents?.FirstOrDefault(e => e.Type == policyType); + var handler = handlers.OfType().FirstOrDefault(e => e.Type == policyType); return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); }); @@ -413,7 +399,7 @@ public class VNextSavePolicyCommandTests .Returns(callInfo => { var policyType = callInfo.Arg(); - var handler = policyValidationEvents?.FirstOrDefault(e => e.Type == policyType); + var handler = handlers.OfType().FirstOrDefault(e => e.Type == policyType); return handler != null ? OneOf.OneOf.FromT0(handler) : OneOf.OneOf.FromT1(new None()); }); @@ -425,7 +411,7 @@ public class VNextSavePolicyCommandTests return new SutProvider() .WithFakeTimeProvider() - .SetDependency(policyDependencyEvents ?? []) + .SetDependency(handlers) .SetDependency(policyEventHandlerFactory) .Create(); } From 449603d180e959e65f4619d004b2960b306f8ac7 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 16 Oct 2025 11:19:48 -0500 Subject: [PATCH 25/52] [PM-26632] - Adding Idempotent Confirm User (#6459) * Added repo call for idempotent user confirm. PLUS TESTS! * Code review changes --- .../IOrganizationUserRepository.cs | 9 ++ .../OrganizationUserRepository.cs | 18 ++- .../OrganizationUserRepository.cs | 20 +++ .../OrganizationUser_ConfirmById.sql | 28 ++++ .../AdminConsole/OrganizationTestHelpers.cs | 46 +++++- .../OrganizationUserRepositoryTests.cs | 142 ++++++++++++++++++ .../2025-10-15_00_OrgUserConfirmById.sql | 28 ++++ 7 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_ConfirmById.sql create mode 100644 util/Migrator/DbScripts/2025-10-15_00_OrgUserConfirmById.sql diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 37a830c92e..b17de3c51d 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -87,4 +87,13 @@ public interface IOrganizationUserRepository : IRepository> GetManyDetailsByRoleAsync(Guid organizationId, OrganizationUserType role); Task CreateManyAsync(IEnumerable organizationUserCollection); + + /// + /// It will only confirm if the user is in the `Accepted` state. + /// + /// This is an idempotent operation. + /// + /// Accepted OrganizationUser to confirm + /// True, if the user was updated. False, if not performed. + Task ConfirmOrganizationUserAsync(OrganizationUser organizationUser); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 5f389ae56d..dc4fc74ff8 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -15,8 +15,6 @@ using Dapper; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -#nullable enable - namespace Bit.Infrastructure.Dapper.Repositories; public class OrganizationUserRepository : Repository, IOrganizationUserRepository @@ -672,4 +670,20 @@ public class OrganizationUserRepository : Repository, IO }, commandType: CommandType.StoredProcedure); } + + public async Task ConfirmOrganizationUserAsync(OrganizationUser organizationUser) + { + await using var connection = new SqlConnection(_marsConnectionString); + + var rowCount = await connection.ExecuteScalarAsync( + $"[{Schema}].[OrganizationUser_ConfirmById]", + new + { + organizationUser.Id, + organizationUser.UserId, + RevisionDate = DateTime.UtcNow.Date + }); + + return rowCount > 0; + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index fae0598c1c..b871ec44bf 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -942,4 +942,24 @@ public class OrganizationUserRepository : Repository ConfirmOrganizationUserAsync(Core.Entities.OrganizationUser organizationUser) + { + using var scope = ServiceScopeFactory.CreateScope(); + await using var dbContext = GetDatabaseContext(scope); + + var result = await dbContext.OrganizationUsers + .Where(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted) + .ExecuteUpdateAsync(x => + x.SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed)); + + if (result <= 0) + { + return false; + } + + await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUser.Id); + return true; + + } } diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ConfirmById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ConfirmById.sql new file mode 100644 index 0000000000..004f1c93eb --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ConfirmById.sql @@ -0,0 +1,28 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ConfirmById] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RowCount INT; + + UPDATE + [dbo].[OrganizationUser] + SET + [Status] = 2, -- Set to Confirmed + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id + AND [Status] = 1 -- Only update if status is Accepted + + SET @RowCount = @@ROWCOUNT; + + IF @RowCount > 0 + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + + SELECT @RowCount; +END diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index 2aee528260..4d5f99f846 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -69,6 +69,42 @@ public static class OrganizationTestHelpers Type = OrganizationUserType.Owner }); + public static Task CreateAcceptedTestOrganizationUserAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization, + User user) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.Owner + }); + + public static Task CreateRevokedTestOrganizationUserAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization, + User user) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Revoked, + Type = OrganizationUserType.Owner + }); + + public static Task CreateConfirmedTestOrganizationUserAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization, + User user) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner + }); + public static Task CreateTestGroupAsync( this IGroupRepository groupRepository, Organization organization, @@ -81,9 +117,9 @@ public static class OrganizationTestHelpers this ICollectionRepository collectionRepository, Organization organization, string identifier = "test") - => collectionRepository.CreateAsync(new Collection - { - OrganizationId = organization.Id, - Name = $"{identifier} {Guid.NewGuid()}" - }); + => collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = $"{identifier} {Guid.NewGuid()}" + }); } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 042d354a87..a60a8e046c 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -1417,4 +1417,146 @@ public class OrganizationUserRepositoryTests // Regular collection should be removed Assert.DoesNotContain(actualCollections, c => c.Id == regularCollection.Id); } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_WhenUserIsAccepted_ReturnsTrue(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IUserRepository userRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user); + + // Act + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + + // Assert + Assert.True(result); + var updatedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); + Assert.NotNull(updatedUser); + Assert.Equal(OrganizationUserStatusType.Confirmed, updatedUser.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_WhenUserIsInvited_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); + + // Act + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + + // Assert + Assert.False(result); + var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); + Assert.NotNull(unchangedUser); + Assert.Equal(OrganizationUserStatusType.Invited, unchangedUser.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_WhenUserIsAlreadyConfirmed_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IUserRepository userRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user); + + // Act + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + + // Assert + Assert.False(result); + var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); + Assert.NotNull(unchangedUser); + Assert.Equal(OrganizationUserStatusType.Confirmed, unchangedUser.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_WhenUserIsRevoked_ReturnsFalse(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IUserRepository userRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateRevokedTestOrganizationUserAsync(organization, user); + + // Act + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + + // Assert + Assert.False(result); + var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); + Assert.NotNull(unchangedUser); + Assert.Equal(OrganizationUserStatusType.Revoked, unchangedUser.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_IsIdempotent_WhenCalledMultipleTimes( + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IUserRepository userRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user); + + // Act - First call should confirm + var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser); + + // Assert + Assert.True(firstResult); + Assert.False(secondResult); + var finalUser = await organizationUserRepository.GetByIdAsync(orgUser.Id); + Assert.NotNull(finalUser); + Assert.Equal(OrganizationUserStatusType.Confirmed, finalUser.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ConfirmOrganizationUserAsync_WhenUserDoesNotExist_ReturnsFalse( + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var nonExistentUser = new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = Guid.NewGuid(), + UserId = Guid.NewGuid(), + Email = "nonexistent@bitwarden.com", + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.Owner + }; + + // Act + var result = await organizationUserRepository.ConfirmOrganizationUserAsync(nonExistentUser); + + // Assert + Assert.False(result); + } } diff --git a/util/Migrator/DbScripts/2025-10-15_00_OrgUserConfirmById.sql b/util/Migrator/DbScripts/2025-10-15_00_OrgUserConfirmById.sql new file mode 100644 index 0000000000..a64cd1401b --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-15_00_OrgUserConfirmById.sql @@ -0,0 +1,28 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ConfirmById] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @RowCount INT; + + UPDATE + [dbo].[OrganizationUser] + SET + [Status] = 2, -- Set to Confirmed + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id + AND [Status] = 1 -- Only update if status is Accepted + + SET @RowCount = @@ROWCOUNT; + + IF @RowCount > 0 + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + + SELECT @RowCount; +END From 2965b499e96d6b5155a4b8239ce82a60fa06759b Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 16 Oct 2025 15:35:14 -0400 Subject: [PATCH 26/52] [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 --- .../SecretsManager/Entities/SecretVersion.cs | 28 + .../Repositories/DatabaseContext.cs | 1 + .../SecretVersionEntityTypeConfiguration.cs | 42 + .../SecretsManager/Models/Secret.cs | 1 + .../SecretsManager/Models/SecretVersion.cs | 24 + .../SecretsManager/Tables/SecretVersion.sql | 27 + ...5-09-26_00_SM_AddSecretVersioningTable.sql | 83 + ...59_CreatingSecretVersionTables.Designer.cs | 3347 ++++++++++++++++ ...51009152659_CreatingSecretVersionTables.cs | 71 + .../DatabaseContextModelSnapshot.cs | 63 + ...12_CreatingSecretVersionTables.Designer.cs | 3353 +++++++++++++++++ ...51009152612_CreatingSecretVersionTables.cs | 69 + .../DatabaseContextModelSnapshot.cs | 63 + ...35_CreatingSecretVersionTables.Designer.cs | 3336 ++++++++++++++++ ...51009152635_CreatingSecretVersionTables.cs | 69 + .../DatabaseContextModelSnapshot.cs | 63 + 16 files changed, 10640 insertions(+) create mode 100644 src/Core/SecretsManager/Entities/SecretVersion.cs create mode 100644 src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretVersionEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/SecretsManager/Models/SecretVersion.cs create mode 100644 src/Sql/dbo/SecretsManager/Tables/SecretVersion.sql create mode 100644 util/Migrator/DbScripts/2025-09-26_00_SM_AddSecretVersioningTable.sql create mode 100644 util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.cs create mode 100644 util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.cs create mode 100644 util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.cs diff --git a/src/Core/SecretsManager/Entities/SecretVersion.cs b/src/Core/SecretsManager/Entities/SecretVersion.cs new file mode 100644 index 0000000000..cee447bd2a --- /dev/null +++ b/src/Core/SecretsManager/Entities/SecretVersion.cs @@ -0,0 +1,28 @@ +#nullable enable +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.SecretsManager.Entities; + +public class SecretVersion : ITableObject +{ + public Guid Id { get; set; } + + public Guid SecretId { get; set; } + + public string Value { get; set; } = string.Empty; + + public DateTime VersionDate { get; set; } + + public Guid? EditorServiceAccountId { get; set; } + + public Guid? EditorOrganizationUserId { get; set; } + + public void SetNewId() + { + if (Id == default(Guid)) + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 7446abdd97..0b3393ca78 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -63,6 +63,7 @@ public class DatabaseContext : DbContext public DbSet Policies { get; set; } public DbSet Providers { get; set; } public DbSet Secret { get; set; } + public DbSet SecretVersion { get; set; } public DbSet ServiceAccount { get; set; } public DbSet Project { get; set; } public DbSet ProviderUsers { get; set; } diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretVersionEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretVersionEntityTypeConfiguration.cs new file mode 100644 index 0000000000..069c7e2450 --- /dev/null +++ b/src/Infrastructure.EntityFramework/SecretsManager/Configurations/SecretVersionEntityTypeConfiguration.cs @@ -0,0 +1,42 @@ +using Bit.Infrastructure.EntityFramework.SecretsManager.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.SecretsManager.Configurations; + +public class SecretVersionEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(sv => sv.Id) + .ValueGeneratedNever(); + + builder.HasKey(sv => sv.Id) + .IsClustered(); + + builder.Property(sv => sv.Value) + .IsRequired(); + + builder.Property(sv => sv.VersionDate) + .IsRequired(); + + builder.HasOne(sv => sv.EditorServiceAccount) + .WithMany() + .HasForeignKey(sv => sv.EditorServiceAccountId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasOne(sv => sv.EditorOrganizationUser) + .WithMany() + .HasForeignKey(sv => sv.EditorOrganizationUserId) + .OnDelete(DeleteBehavior.SetNull); + + builder.HasIndex(sv => sv.SecretId) + .HasDatabaseName("IX_SecretVersion_SecretId"); + + builder.HasIndex(sv => sv.EditorServiceAccountId) + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + builder.HasIndex(sv => sv.EditorOrganizationUserId) + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + } +} diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs index 5992f32135..09d8c389df 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs @@ -13,6 +13,7 @@ public class Secret : Core.SecretsManager.Entities.Secret public virtual ICollection UserAccessPolicies { get; set; } public virtual ICollection GroupAccessPolicies { get; set; } public virtual ICollection ServiceAccountAccessPolicies { get; set; } + public virtual ICollection SecretVersions { get; set; } } public class SecretMapperProfile : Profile diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/SecretVersion.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/SecretVersion.cs new file mode 100644 index 0000000000..d4a364ab0f --- /dev/null +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/SecretVersion.cs @@ -0,0 +1,24 @@ +#nullable enable + +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; + +public class SecretVersion : Core.SecretsManager.Entities.SecretVersion +{ + public Secret? Secret { get; set; } + + public ServiceAccount? EditorServiceAccount { get; set; } + + public Bit.Infrastructure.EntityFramework.Models.OrganizationUser? EditorOrganizationUser { get; set; } +} + +public class SecretVersionMapperProfile : Profile +{ + public SecretVersionMapperProfile() + { + CreateMap() + .PreserveReferences() + .ReverseMap(); + } +} diff --git a/src/Sql/dbo/SecretsManager/Tables/SecretVersion.sql b/src/Sql/dbo/SecretsManager/Tables/SecretVersion.sql new file mode 100644 index 0000000000..31ab443f56 --- /dev/null +++ b/src/Sql/dbo/SecretsManager/Tables/SecretVersion.sql @@ -0,0 +1,27 @@ +CREATE TABLE [dbo].[SecretVersion] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [SecretId] UNIQUEIDENTIFIER NOT NULL, + [Value] NVARCHAR (MAX) NOT NULL, + [VersionDate] DATETIME2 (7) NOT NULL, + [EditorServiceAccountId] UNIQUEIDENTIFIER NULL, + [EditorOrganizationUserId] UNIQUEIDENTIFIER NULL, + CONSTRAINT [PK_SecretVersion] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_SecretVersion_OrganizationUser] FOREIGN KEY ([EditorOrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id]) ON DELETE SET NULL, + CONSTRAINT [FK_SecretVersion_Secret] FOREIGN KEY ([SecretId]) REFERENCES [dbo].[Secret] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_SecretVersion_ServiceAccount] FOREIGN KEY ([EditorServiceAccountId]) REFERENCES [dbo].[ServiceAccount] ([Id]) ON DELETE SET NULL +); + +GO +CREATE NONCLUSTERED INDEX [IX_SecretVersion_SecretId] + ON [dbo].[SecretVersion]([SecretId] ASC); + +GO +CREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorServiceAccountId] + ON [dbo].[SecretVersion]([EditorServiceAccountId] ASC) + WHERE [EditorServiceAccountId] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorOrganizationUserId] + ON [dbo].[SecretVersion]([EditorOrganizationUserId] ASC) + WHERE [EditorOrganizationUserId] IS NOT NULL; +GO \ No newline at end of file diff --git a/util/Migrator/DbScripts/2025-09-26_00_SM_AddSecretVersioningTable.sql b/util/Migrator/DbScripts/2025-09-26_00_SM_AddSecretVersioningTable.sql new file mode 100644 index 0000000000..c039553ce6 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-26_00_SM_AddSecretVersioningTable.sql @@ -0,0 +1,83 @@ +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'SecretVersion' AND schema_id = SCHEMA_ID('dbo')) +BEGIN + CREATE TABLE [dbo].[SecretVersion] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [SecretId] UNIQUEIDENTIFIER NOT NULL, + [Value] NVARCHAR (MAX) NOT NULL, + [VersionDate] DATETIME2 (7) NOT NULL, + [EditorServiceAccountId] UNIQUEIDENTIFIER NULL, + [EditorOrganizationUserId] UNIQUEIDENTIFIER NULL, + CONSTRAINT [PK_SecretVersion] PRIMARY KEY CLUSTERED ([Id] ASC) + ); +END +GO + +-- Ensure foreign keys exist +IF NOT EXISTS ( + SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_SecretVersion_OrganizationUser' +) +BEGIN + ALTER TABLE [dbo].[SecretVersion] + ADD CONSTRAINT [FK_SecretVersion_OrganizationUser] + FOREIGN KEY ([EditorOrganizationUserId]) + REFERENCES [dbo].[OrganizationUser] ([Id]) + ON DELETE SET NULL; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_SecretVersion_Secret' +) +BEGIN + ALTER TABLE [dbo].[SecretVersion] + ADD CONSTRAINT [FK_SecretVersion_Secret] + FOREIGN KEY ([SecretId]) + REFERENCES [dbo].[Secret] ([Id]) + ON DELETE CASCADE; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_SecretVersion_ServiceAccount' +) +BEGIN + ALTER TABLE [dbo].[SecretVersion] + ADD CONSTRAINT [FK_SecretVersion_ServiceAccount] + FOREIGN KEY ([EditorServiceAccountId]) + REFERENCES [dbo].[ServiceAccount] ([Id]) + ON DELETE SET NULL; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes WHERE name = 'IX_SecretVersion_SecretId' AND object_id = OBJECT_ID('[dbo].[SecretVersion]') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_SecretVersion_SecretId] + ON [dbo].[SecretVersion]([SecretId] ASC); +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes WHERE name = 'IX_SecretVersion_EditorServiceAccountId' AND object_id = OBJECT_ID('[dbo].[SecretVersion]') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorServiceAccountId] + ON [dbo].[SecretVersion]([EditorServiceAccountId] ASC) + WHERE [EditorServiceAccountId] IS NOT NULL; +END +GO + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes WHERE name = 'IX_SecretVersion_EditorOrganizationUserId' AND object_id = OBJECT_ID('[dbo].[SecretVersion]') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_SecretVersion_EditorOrganizationUserId] + ON [dbo].[SecretVersion]([EditorOrganizationUserId] ASC) + WHERE [EditorOrganizationUserId] IS NOT NULL; +END +GO diff --git a/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.Designer.cs b/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.Designer.cs new file mode 100644 index 0000000000..8683a835b5 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.Designer.cs @@ -0,0 +1,3347 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251009152659_CreatingSecretVersionTables")] + partial class CreatingSecretVersionTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ArchivedDate") + .HasColumnType("datetime(6)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.cs b/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.cs new file mode 100644 index 0000000000..7e3ae2eebe --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251009152659_CreatingSecretVersionTables.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class CreatingSecretVersionTables : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecretVersion", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + SecretId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + VersionDate = table.Column(type: "datetime(6)", nullable: false), + EditorServiceAccountId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + EditorOrganizationUserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci") + }, + constraints: table => + { + table.PrimaryKey("PK_SecretVersion", x => x.Id); + table.ForeignKey( + name: "FK_SecretVersion_OrganizationUser_EditorOrganizationUserId", + column: x => x.EditorOrganizationUserId, + principalTable: "OrganizationUser", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_SecretVersion_Secret_SecretId", + column: x => x.SecretId, + principalTable: "Secret", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SecretVersion_ServiceAccount_EditorServiceAccountId", + column: x => x.EditorServiceAccountId, + principalTable: "ServiceAccount", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorOrganizationUserId", + table: "SecretVersion", + column: "EditorOrganizationUserId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorServiceAccountId", + table: "SecretVersion", + column: "EditorServiceAccountId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_SecretId", + table: "SecretVersion", + column: "SecretId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecretVersion"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index dce61f805c..842ce555e5 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2159,6 +2159,42 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("Secret", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.Property("Id") @@ -2984,6 +3020,31 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -3255,6 +3316,8 @@ namespace Bit.MySqlMigrations.Migrations { b.Navigation("GroupAccessPolicies"); + b.Navigation("SecretVersions"); + b.Navigation("ServiceAccountAccessPolicies"); b.Navigation("UserAccessPolicies"); diff --git a/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.Designer.cs b/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.Designer.cs new file mode 100644 index 0000000000..438f707f20 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.Designer.cs @@ -0,0 +1,3353 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251009152612_CreatingSecretVersionTables")] + partial class CreatingSecretVersionTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ArchivedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.cs b/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.cs new file mode 100644 index 0000000000..36036dd1c5 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251009152612_CreatingSecretVersionTables.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class CreatingSecretVersionTables : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecretVersion", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + SecretId = table.Column(type: "uuid", nullable: false), + Value = table.Column(type: "text", nullable: false), + VersionDate = table.Column(type: "timestamp with time zone", nullable: false), + EditorServiceAccountId = table.Column(type: "uuid", nullable: true), + EditorOrganizationUserId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SecretVersion", x => x.Id); + table.ForeignKey( + name: "FK_SecretVersion_OrganizationUser_EditorOrganizationUserId", + column: x => x.EditorOrganizationUserId, + principalTable: "OrganizationUser", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_SecretVersion_Secret_SecretId", + column: x => x.SecretId, + principalTable: "Secret", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SecretVersion_ServiceAccount_EditorServiceAccountId", + column: x => x.EditorServiceAccountId, + principalTable: "ServiceAccount", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorOrganizationUserId", + table: "SecretVersion", + column: "EditorOrganizationUserId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorServiceAccountId", + table: "SecretVersion", + column: "EditorServiceAccountId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_SecretId", + table: "SecretVersion", + column: "SecretId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecretVersion"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index c6ed007410..2279ae43af 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2165,6 +2165,42 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("Secret", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.Property("Id") @@ -2990,6 +3026,31 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -3261,6 +3322,8 @@ namespace Bit.PostgresMigrations.Migrations { b.Navigation("GroupAccessPolicies"); + b.Navigation("SecretVersions"); + b.Navigation("ServiceAccountAccessPolicies"); b.Navigation("UserAccessPolicies"); diff --git a/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.Designer.cs b/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.Designer.cs new file mode 100644 index 0000000000..e3338b351c --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.Designer.cs @@ -0,0 +1,3336 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251009152635_CreatingSecretVersionTables")] + partial class CreatingSecretVersionTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ArchivedDate") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("SecretVersions"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.cs b/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.cs new file mode 100644 index 0000000000..0735092023 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251009152635_CreatingSecretVersionTables.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class CreatingSecretVersionTables : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SecretVersion", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + SecretId = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: false), + VersionDate = table.Column(type: "TEXT", nullable: false), + EditorServiceAccountId = table.Column(type: "TEXT", nullable: true), + EditorOrganizationUserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SecretVersion", x => x.Id); + table.ForeignKey( + name: "FK_SecretVersion_OrganizationUser_EditorOrganizationUserId", + column: x => x.EditorOrganizationUserId, + principalTable: "OrganizationUser", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "FK_SecretVersion_Secret_SecretId", + column: x => x.SecretId, + principalTable: "Secret", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SecretVersion_ServiceAccount_EditorServiceAccountId", + column: x => x.EditorServiceAccountId, + principalTable: "ServiceAccount", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorOrganizationUserId", + table: "SecretVersion", + column: "EditorOrganizationUserId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_EditorServiceAccountId", + table: "SecretVersion", + column: "EditorServiceAccountId"); + + migrationBuilder.CreateIndex( + name: "IX_SecretVersion_SecretId", + table: "SecretVersion", + column: "SecretId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SecretVersion"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 494431b932..dc66860e6c 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2148,6 +2148,42 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("Secret", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.Property("Id") @@ -2973,6 +3009,31 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") @@ -3244,6 +3305,8 @@ namespace Bit.SqliteMigrations.Migrations { b.Navigation("GroupAccessPolicies"); + b.Navigation("SecretVersions"); + b.Navigation("ServiceAccountAccessPolicies"); b.Navigation("UserAccessPolicies"); From 652da9644d9b0b77b8fb1030350199fa4cf18ca1 Mon Sep 17 00:00:00 2001 From: mpbw2 <59324545+mpbw2@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:33:01 -0400 Subject: [PATCH 27/52] [PM-20036][PM-20153][PM-20156][PM-26075] remove mobile feature flags (#6380) --- src/Core/Constants.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d0d63ff787..7b848845fb 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -201,22 +201,15 @@ public static class FeatureFlagKeys public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption"; /* Mobile Team */ - public const string NativeCarouselFlow = "native-carousel-flow"; - public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string AndroidImportLoginsFlow = "import-logins-flow"; - public const string AppReviewPrompt = "app-review-prompt"; public const string AndroidMutualTls = "mutual-tls"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; - public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync"; public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias"; public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias"; - public const string EnablePMFlightRecorder = "enable-pm-flight-recorder"; public const string MobileErrorReporting = "mobile-error-reporting"; public const string AndroidChromeAutofill = "android-chrome-autofill"; public const string UserManagedPrivilegedApps = "pm-18970-user-managed-privileged-apps"; - public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings"; - public const string AppIntents = "app-intents"; public const string SendAccess = "pm-19394-send-access-control"; public const string CxpImportMobile = "cxp-import-mobile"; public const string CxpExportMobile = "cxp-export-mobile"; From 39f38f50317844fb2aab79adc8f6a2583217a40a Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:44:04 +0200 Subject: [PATCH 28/52] Add feature flag for chromium importer with ABE (#6464) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7b848845fb..26beb14c9c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -222,6 +222,7 @@ public static class FeatureFlagKeys /* Tools Team */ public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators"; + public const string ChromiumImporterWithABE = "pm-25855-chromium-importer-abe"; /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; From a68e2b9eb5ad1ff8c21f11afd676cb9f1a3db001 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 20 Oct 2025 10:34:57 +0000 Subject: [PATCH 29/52] Bumped version to 2025.10.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4aa72f3e81..84b8dd22be 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.10.0 + 2025.10.1 Bit.$(MSBuildProjectName) enable From 4bf7cf956b235129eeae79598037030d1076c664 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 20 Oct 2025 12:51:08 +0200 Subject: [PATCH 30/52] [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> --- .../Auth/Controllers/AccountsController.cs | 14 +- .../Billing/Controllers/AccountsController.cs | 8 +- src/Api/Controllers/UsersController.cs | 33 - .../AccountsKeyManagementController.cs | 3 +- .../Controllers/UsersController.cs | 39 + .../Requests/AccountKeysRequestModel.cs | 43 +- .../Requests/KeyRegenerationRequestModel.cs | 3 +- .../PublicKeyEncryptionKeyPairRequestModel.cs | 20 + .../RotateAccountKeysAndDataRequestModel.cs | 3 +- .../Requests/SignatureKeyPairRequestModel.cs | 28 + .../Models/Requests/UnlockDataRequestModel.cs | 3 +- .../Models/Requests/UserDataRequestModel.cs | 3 +- .../Models/Response/ProfileResponseModel.cs | 6 + src/Api/Vault/Controllers/SyncController.cs | 16 +- .../Models/Response/SyncResponseModel.cs | 6 +- .../Api/Response/UserDecryptionOptions.cs | 2 +- src/Core/Constants.cs | 1 + src/Core/Entities/User.cs | 41 + .../Entities/UserSignatureKeyPair.cs | 30 + .../KeyManagement/Enums/SignatureAlgorithm.cs | 9 + ...eyManagementServiceCollectionExtensions.cs | 8 + .../Models/Api/Request/SecurityStateModel.cs | 32 + .../MasterPasswordUnlockResponseModel.cs | 2 +- .../Api/Response/PrivateKeysResponseModel.cs | 48 + ...PublicKeyEncryptionKeyPairResponseModel.cs | 34 + .../Api/Response/PublicKeysResponseModel.cs | 30 + .../Response/SignatureKeyPairResponseModel.cs | 32 + .../Response/UserDecryptionResponseModel.cs | 2 +- .../Data/PublicKeyEncryptionKeyPairData.cs | 20 + .../Models/Data/RotateUserAccountKeysData.cs | 26 +- .../Models/Data/SecurityStateData.cs | 10 + .../Models/Data/SignatureKeyPairData.cs | 21 + .../Models/Data/UserAccountKeysData.cs | 9 + .../Interfaces/IUserAcountKeysQuery.cs | 10 + .../Queries/UserAccountKeysQuery.cs | 35 + .../IUserSignatureKeyPairRepository.cs | 14 + .../RotateUserAccountkeysCommand.cs | 187 +- .../RequestValidators/BaseRequestValidator.cs | 9 +- .../CustomTokenRequestValidator.cs | 7 +- .../ResourceOwnerPasswordValidator.cs | 7 +- .../WebAuthnGrantValidator.cs | 7 +- .../UserDecryptionOptionsBuilder.cs | 2 +- .../DapperServiceCollectionExtensions.cs | 1 + .../UserSignatureKeyPairRepository.cs | 79 + ...ityFrameworkServiceCollectionExtensions.cs | 1 + ...SignatureKeyPairEntityTypeConfiguration.cs | 22 + .../Models/UserSignatureKeyPair.cs | 19 + .../UserSignatureKeyPairRepository.cs | 66 + .../Repositories/DatabaseContext.cs | 1 + .../UserSignatureKeyPair_ReadByUserId.sql | 13 + .../UserSignatureKeyPair_SetForRotation.sql | 33 + ...UserSignatureKeyPair_UpdateForRotation.sql | 19 + .../Tables/UserSignatureKeyPair.sql | 16 + .../Views/UserSignatureKeyPairView.sql | 6 + src/Sql/dbo/Stored Procedures/User_Create.sql | 15 +- src/Sql/dbo/Stored Procedures/User_Update.sql | 10 +- src/Sql/dbo/Tables/User.sql | 3 + .../AccountsKeyManagementControllerTests.cs | 202 + .../Controllers/AccountsControllerTests.cs | 5 +- .../AccountsKeyManagementControllerTests.cs | 59 +- .../Controllers/UsersControllerTests.cs | 112 + .../Request/SignatureKeyPairRequestModel.cs | 22 + .../Vault/Controllers/SyncControllerTests.cs | 36 +- .../Queries/UserAccountKeysQuery.cs | 43 + .../RotateUserAccountKeysCommandTests.cs | 408 +- .../BaseRequestValidatorTests.cs | 205 +- .../BaseRequestValidatorTestWrapper.cs | 7 +- .../EntityFrameworkRepositoryFixtures.cs | 1 + .../EqualityComparers/UserCompare.cs | 3 +- .../DbScripts/2025-10-13_00_UserCryptoV2.sql | 396 ++ .../20251013083703_UserCryptoV2.Designer.cs | 3338 ++++++++++++++++ .../Migrations/20251013083703_UserCryptoV2.cs | 84 + .../DatabaseContextModelSnapshot.cs | 54 + .../20251013083502_UserCryptoV2.Designer.cs | 3344 +++++++++++++++++ .../Migrations/20251013083502_UserCryptoV2.cs | 79 + .../DatabaseContextModelSnapshot.cs | 54 + .../20251013083514_UserCryptoV2.Designer.cs | 3327 ++++++++++++++++ .../Migrations/20251013083514_UserCryptoV2.cs | 79 + .../DatabaseContextModelSnapshot.cs | 54 + 79 files changed, 12918 insertions(+), 161 deletions(-) delete mode 100644 src/Api/Controllers/UsersController.cs create mode 100644 src/Api/KeyManagement/Controllers/UsersController.cs create mode 100644 src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs create mode 100644 src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs create mode 100644 src/Core/KeyManagement/Enums/SignatureAlgorithm.cs create mode 100644 src/Core/KeyManagement/Models/Api/Request/SecurityStateModel.cs rename src/Core/KeyManagement/Models/{ => Api}/Response/MasterPasswordUnlockResponseModel.cs (91%) create mode 100644 src/Core/KeyManagement/Models/Api/Response/PrivateKeysResponseModel.cs create mode 100644 src/Core/KeyManagement/Models/Api/Response/PublicKeyEncryptionKeyPairResponseModel.cs create mode 100644 src/Core/KeyManagement/Models/Api/Response/PublicKeysResponseModel.cs create mode 100644 src/Core/KeyManagement/Models/Api/Response/SignatureKeyPairResponseModel.cs rename src/Core/KeyManagement/Models/{ => Api}/Response/UserDecryptionResponseModel.cs (82%) create mode 100644 src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs create mode 100644 src/Core/KeyManagement/Models/Data/SecurityStateData.cs create mode 100644 src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs create mode 100644 src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs create mode 100644 src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs create mode 100644 src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs create mode 100644 src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs create mode 100644 src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs create mode 100644 src/Infrastructure.EntityFramework/KeyManagement/Configurations/UserSignatureKeyPairEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/KeyManagement/Models/UserSignatureKeyPair.cs create mode 100644 src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs create mode 100644 src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql create mode 100644 src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql create mode 100644 src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql create mode 100644 src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql create mode 100644 src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql create mode 100644 test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs create mode 100644 test/Api.Test/KeyManagement/Models/Request/SignatureKeyPairRequestModel.cs create mode 100644 test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs create mode 100644 util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql create mode 100644 util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.cs create mode 100644 util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.cs create mode 100644 util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 19165a5a1c..ecf49c18c8 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -18,6 +18,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; @@ -40,6 +41,7 @@ public class AccountsController : Controller private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; + private readonly IUserAccountKeysQuery _userAccountKeysQuery; private readonly ITwoFactorEmailService _twoFactorEmailService; private readonly IChangeKdfCommand _changeKdfCommand; @@ -53,6 +55,7 @@ public class AccountsController : Controller ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, + IUserAccountKeysQuery userAccountKeysQuery, ITwoFactorEmailService twoFactorEmailService, IChangeKdfCommand changeKdfCommand ) @@ -66,6 +69,7 @@ public class AccountsController : Controller _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; + _userAccountKeysQuery = userAccountKeysQuery; _twoFactorEmailService = twoFactorEmailService; _changeKdfCommand = changeKdfCommand; } @@ -332,7 +336,9 @@ public class AccountsController : Controller var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); - var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, + var accountKeys = await _userAccountKeysQuery.Run(user); + + var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser); return response; @@ -364,8 +370,9 @@ public class AccountsController : Controller var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); + var userAccountKeys = await _userAccountKeysQuery.Run(user); - var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser); + var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser); return response; } @@ -389,8 +396,9 @@ public class AccountsController : Controller var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); + var accountKeys = await _userAccountKeysQuery.Run(user); - var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); + var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); return response; } diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 9411d454aa..9dbe4a5532 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -1,4 +1,5 @@ #nullable enable + using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; @@ -8,6 +9,7 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; @@ -21,7 +23,8 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class AccountsController( IUserService userService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IUserAccountKeysQuery userAccountKeysQuery) : Controller { [HttpPost("premium")] public async Task PostPremiumAsync( @@ -58,8 +61,9 @@ public class AccountsController( var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); + var accountKeys = await userAccountKeysQuery.Run(user); - var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, + var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); return new PaymentResponseModel { diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs deleted file mode 100644 index 4dfd047d37..0000000000 --- a/src/Api/Controllers/UsersController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Bit.Api.Models.Response; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Controllers; - -[Route("users")] -[Authorize("Application")] -public class UsersController : Controller -{ - private readonly IUserRepository _userRepository; - - public UsersController( - IUserRepository userRepository) - { - _userRepository = userRepository; - } - - [HttpGet("{id}/public-key")] - public async Task Get(string id) - { - var guidId = new Guid(id); - var key = await _userRepository.GetPublicKeyAsync(guidId); - if (key == null) - { - throw new NotFoundException(); - } - - return new UserKeyResponseModel(guidId, key); - } -} diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index 9fc0e9a75a..7968970048 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -106,8 +106,7 @@ public class AccountsKeyManagementController : Controller { OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash, - UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey, - AccountPublicKey = model.AccountKeys.AccountPublicKey, + AccountKeys = model.AccountKeys.ToAccountKeysData(), MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData), diff --git a/src/Api/KeyManagement/Controllers/UsersController.cs b/src/Api/KeyManagement/Controllers/UsersController.cs new file mode 100644 index 0000000000..cfd2f8ee29 --- /dev/null +++ b/src/Api/KeyManagement/Controllers/UsersController.cs @@ -0,0 +1,39 @@ +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Queries.Interfaces; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel; + + +namespace Bit.Api.KeyManagement.Controllers; + +[Route("users")] +[Authorize("Application")] +public class UsersController : Controller +{ + private readonly IUserRepository _userRepository; + private readonly IUserAccountKeysQuery _userAccountKeysQuery; + + public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery) + { + _userRepository = userRepository; + _userAccountKeysQuery = userAccountKeysQuery; + } + + [HttpGet("{id}/public-key")] + public async Task GetPublicKeyAsync([FromRoute] Guid id) + { + var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException(); + return new UserKeyResponseModel(id, key); + } + + [HttpGet("{id}/keys")] + public async Task GetAccountKeysAsync([FromRoute] Guid id) + { + var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException(); + var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found."); + return new PublicKeysResponseModel(accountKeys); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs index 7c7de4d210..b64e826911 100644 --- a/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs @@ -1,4 +1,5 @@ -#nullable enable +using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; namespace Bit.Api.KeyManagement.Models.Requests; @@ -7,4 +8,44 @@ public class AccountKeysRequestModel { [EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; } public required string AccountPublicKey { get; set; } + + public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; } + public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; } + public SecurityStateModel? SecurityState { get; set; } + + public UserAccountKeysData ToAccountKeysData() + { + // This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required. + // TODO: https://bitwarden.atlassian.net/browse/PM-23751 + if (PublicKeyEncryptionKeyPair == null) + { + return new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData + ( + UserKeyEncryptedAccountPrivateKey, + AccountPublicKey + ), + }; + } + else + { + if (SignatureKeyPair == null || SecurityState == null) + { + return new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(), + }; + } + else + { + return new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(), + SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(), + SecurityStateData = SecurityState.ToSecurityState() + }; + } + } + } } diff --git a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs index 495d13cccd..767cfd3f9b 100644 --- a/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/KeyRegenerationRequestModel.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; namespace Bit.Api.KeyManagement.Models.Requests; diff --git a/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs b/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs new file mode 100644 index 0000000000..24c1e6a946 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs @@ -0,0 +1,20 @@ +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class PublicKeyEncryptionKeyPairRequestModel +{ + [EncryptedString] public required string WrappedPrivateKey { get; set; } + public required string PublicKey { get; set; } + public string? SignedPublicKey { get; set; } + + public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData() + { + return new PublicKeyEncryptionKeyPairData( + WrappedPrivateKey, + PublicKey, + SignedPublicKey + ); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs index b0b19e2bd3..02780b015a 100644 --- a/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Bit.Api.KeyManagement.Models.Requests; diff --git a/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs b/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs new file mode 100644 index 0000000000..3cdb4f53f1 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs @@ -0,0 +1,28 @@ +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class SignatureKeyPairRequestModel +{ + public required string SignatureAlgorithm { get; set; } + [EncryptedString] public required string WrappedSigningKey { get; set; } + public required string VerifyingKey { get; set; } + + public SignatureKeyPairData ToSignatureKeyPairData() + { + if (SignatureAlgorithm != "ed25519") + { + throw new ArgumentException( + $"Unsupported signature algorithm: {SignatureAlgorithm}" + ); + } + var algorithm = Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519; + + return new SignatureKeyPairData( + algorithm, + WrappedSigningKey, + VerifyingKey + ); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs index 3af944110c..01e5dd7017 100644 --- a/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.WebAuthn; diff --git a/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs index f854d82bcc..df922fcda0 100644 --- a/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Api.Tools.Models.Request; +using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; namespace Bit.Api.KeyManagement.Models.Requests; diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index cbdfaf0f16..30ba05b6a6 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -5,6 +5,8 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Api; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -13,6 +15,7 @@ namespace Bit.Api.Models.Response; public class ProfileResponseModel : ResponseModel { public ProfileResponseModel(User user, + UserAccountKeysData userAccountKeysData, IEnumerable organizationsUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -35,6 +38,7 @@ public class ProfileResponseModel : ResponseModel TwoFactorEnabled = twoFactorEnabled; Key = user.Key; PrivateKey = user.PrivateKey; + AccountKeys = userAccountKeysData != null ? new PrivateKeysResponseModel(userAccountKeysData) : null; SecurityStamp = user.SecurityStamp; ForcePasswordReset = user.ForcePasswordReset; UsesKeyConnector = user.UsesKeyConnector; @@ -60,7 +64,9 @@ public class ProfileResponseModel : ResponseModel public string Culture { get; set; } public bool TwoFactorEnabled { get; set; } public string Key { get; set; } + [Obsolete("Use AccountKeys instead.")] public string PrivateKey { get; set; } + public PrivateKeysResponseModel AccountKeys { get; set; } public string SecurityStamp { get; set; } public bool ForcePasswordReset { get; set; } public bool UsesKeyConnector { get; set; } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 54f1b9e70b..6ac8d06ba0 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -11,6 +11,8 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; @@ -42,6 +44,7 @@ public class SyncController : Controller private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IUserAccountKeysQuery _userAccountKeysQuery; public SyncController( IUserService userService, @@ -57,7 +60,8 @@ public class SyncController : Controller ICurrentContext currentContext, IFeatureService featureService, IApplicationCacheService applicationCacheService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IUserAccountKeysQuery userAccountKeysQuery) { _userService = userService; _folderRepository = folderRepository; @@ -73,6 +77,7 @@ public class SyncController : Controller _featureService = featureService; _applicationCacheService = applicationCacheService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _userAccountKeysQuery = userAccountKeysQuery; } [HttpGet("")] @@ -116,7 +121,14 @@ public class SyncController : Controller var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, + UserAccountKeysData userAccountKeys = null; + // JIT TDE users and some broken/old users may not have a private key. + if (!string.IsNullOrWhiteSpace(user.PrivateKey)) + { + userAccountKeys = await _userAccountKeysQuery.Run(user); + } + + var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index e19defce51..1981ac834e 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -7,7 +7,8 @@ using Bit.Api.Tools.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Entities; -using Bit.Core.KeyManagement.Models.Response; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Api; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; @@ -24,6 +25,7 @@ public class SyncResponseModel() : ResponseModel("sync") public SyncResponseModel( GlobalSettings globalSettings, User user, + UserAccountKeysData userAccountKeysData, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, IDictionary organizationAbilities, @@ -40,7 +42,7 @@ public class SyncResponseModel() : ResponseModel("sync") IEnumerable sends) : this() { - Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, + Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(cipher => diff --git a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs index bd8542e8bf..aa8a298200 100644 --- a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs +++ b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs @@ -1,5 +1,5 @@ using System.Text.Json.Serialization; -using Bit.Core.KeyManagement.Models.Response; +using Bit.Core.KeyManagement.Models.Api.Response; using Bit.Core.Models.Api; namespace Bit.Core.Auth.Models.Api.Response; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 26beb14c9c..0cf4a4040e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -192,6 +192,7 @@ public static class FeatureFlagKeys public const string UserkeyRotationV2 = "userkey-rotation-v2"; public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public const string UserSdkForDecryption = "use-sdk-for-decryption"; + public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation"; public const string PM17987_BlockType0 = "pm-17987-block-type-0"; public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings"; public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data"; diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 12c527ed78..fec9b80d8e 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; @@ -21,6 +22,9 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac [MaxLength(256)] public string Email { get; set; } = null!; public bool EmailVerified { get; set; } + /// + /// The server-side master-password hash + /// [MaxLength(300)] public string? MasterPassword { get; set; } [MaxLength(50)] @@ -41,9 +45,30 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac /// organization membership. /// public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow; + /// + /// The master-password-sealed user key. + /// public string? Key { get; set; } + /// + /// The raw public key, without a signature from the user's signature key. + /// public string? PublicKey { get; set; } + /// + /// User key wrapped private key. + /// public string? PrivateKey { get; set; } + /// + /// The public key, signed by the user's signature key. + /// + public string? SignedPublicKey { get; set; } + /// + /// The security version is included in the security state, but needs COSE parsing + /// + public int? SecurityVersion { get; set; } + /// + /// The security state is a signed object attesting to the version of the user's account. + /// + public string? SecurityState { get; set; } public bool Premium { get; set; } public DateTime? PremiumExpirationDate { get; set; } public DateTime? RenewalReminderDate { get; set; } @@ -180,6 +205,12 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac return Premium; } + public int GetSecurityVersion() + { + // If no security version is set, it is version 1. The minimum initialized version is 2. + return SecurityVersion ?? 1; + } + /// /// Serializes the C# object to the User.TwoFactorProviders property in JSON format. /// @@ -243,4 +274,14 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac { return MasterPassword != null; } + + public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair() + { + if (string.IsNullOrWhiteSpace(PrivateKey) || string.IsNullOrWhiteSpace(PublicKey)) + { + throw new InvalidOperationException("User public key encryption key pair is not fully initialized."); + } + + return new PublicKeyEncryptionKeyPairData(PrivateKey, PublicKey, SignedPublicKey); + } } diff --git a/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs new file mode 100644 index 0000000000..dada9e0d7a --- /dev/null +++ b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs @@ -0,0 +1,30 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Enums; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + + +namespace Bit.Core.KeyManagement.Entities; + +public class UserSignatureKeyPair : ITableObject, IRevisable +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public SignatureAlgorithm SignatureAlgorithm { get; set; } + + public required string VerifyingKey { get; set; } + public required string SigningKey { get; set; } + + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + + public SignatureKeyPairData ToSignatureKeyPairData() + { + return new SignatureKeyPairData(SignatureAlgorithm, SigningKey, VerifyingKey); + } +} diff --git a/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs b/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs new file mode 100644 index 0000000000..9216c3f489 --- /dev/null +++ b/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.KeyManagement.Enums; + +// +// Represents the algorithm / digital signature scheme used for a signature key pair. +// +public enum SignatureAlgorithm : byte +{ + Ed25519 = 0 +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index e4ebdb4860..0e551c5d0e 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -2,6 +2,8 @@ using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Kdf; using Bit.Core.KeyManagement.Kdf.Implementations; +using Bit.Core.KeyManagement.Queries; +using Bit.Core.KeyManagement.Queries.Interfaces; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.KeyManagement; @@ -11,6 +13,7 @@ public static class KeyManagementServiceCollectionExtensions public static void AddKeyManagementServices(this IServiceCollection services) { services.AddKeyManagementCommands(); + services.AddKeyManagementQueries(); services.AddSendPasswordServices(); } @@ -19,4 +22,9 @@ public static class KeyManagementServiceCollectionExtensions services.AddScoped(); services.AddScoped(); } + + private static void AddKeyManagementQueries(this IServiceCollection services) + { + services.AddScoped(); + } } diff --git a/src/Core/KeyManagement/Models/Api/Request/SecurityStateModel.cs b/src/Core/KeyManagement/Models/Api/Request/SecurityStateModel.cs new file mode 100644 index 0000000000..1acb52146e --- /dev/null +++ b/src/Core/KeyManagement/Models/Api/Request/SecurityStateModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Models.Api.Request; + +public class SecurityStateModel +{ + [StringLength(1000)] + [JsonPropertyName("securityState")] + public required string SecurityState { get; set; } + [JsonPropertyName("securityVersion")] + public required int SecurityVersion { get; set; } + + public SecurityStateData ToSecurityState() + { + return new SecurityStateData + { + SecurityState = SecurityState, + SecurityVersion = SecurityVersion + }; + } + + public static SecurityStateModel FromSecurityStateData(SecurityStateData data) + { + return new SecurityStateModel + { + SecurityState = data.SecurityState, + SecurityVersion = data.SecurityVersion + }; + } +} diff --git a/src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/MasterPasswordUnlockResponseModel.cs similarity index 91% rename from src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs rename to src/Core/KeyManagement/Models/Api/Response/MasterPasswordUnlockResponseModel.cs index f7d5dee852..f54e88c596 100644 --- a/src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs +++ b/src/Core/KeyManagement/Models/Api/Response/MasterPasswordUnlockResponseModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Utilities; -namespace Bit.Core.KeyManagement.Models.Response; +namespace Bit.Core.KeyManagement.Models.Api.Response; public class MasterPasswordUnlockResponseModel { diff --git a/src/Core/KeyManagement/Models/Api/Response/PrivateKeysResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/PrivateKeysResponseModel.cs new file mode 100644 index 0000000000..bcee4c0ada --- /dev/null +++ b/src/Core/KeyManagement/Models/Api/Response/PrivateKeysResponseModel.cs @@ -0,0 +1,48 @@ +using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Core.KeyManagement.Models.Api.Response; + + +/// +/// This response model is used to return the asymmetric encryption keys, +/// and signature keys of an entity. This includes the private keys of the key pairs, +/// (private key, signing key), and the public keys of the key pairs (unsigned public key, +/// signed public key, verification key). +/// +public class PrivateKeysResponseModel : ResponseModel +{ + // Not all accounts have signature keys, but all accounts have public encryption keys. + [JsonPropertyName("signatureKeyPair")] + public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; } + + [JsonPropertyName("publicKeyEncryptionKeyPair")] + public required PublicKeyEncryptionKeyPairResponseModel PublicKeyEncryptionKeyPair { get; set; } + + [JsonPropertyName("securityState")] + public SecurityStateModel? SecurityState { get; set; } + + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys") + { + ArgumentNullException.ThrowIfNull(accountKeys); + PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairResponseModel(accountKeys.PublicKeyEncryptionKeyPairData); + + if (accountKeys.SignatureKeyPairData != null && accountKeys.SecurityStateData != null) + { + SignatureKeyPair = new SignatureKeyPairResponseModel(accountKeys.SignatureKeyPairData); + SecurityState = SecurityStateModel.FromSecurityStateData(accountKeys.SecurityStateData!); + } + } + + [JsonConstructor] + public PrivateKeysResponseModel(SignatureKeyPairResponseModel? signatureKeyPair, PublicKeyEncryptionKeyPairResponseModel publicKeyEncryptionKeyPair, SecurityStateModel? securityState) + : base("privateKeys") + { + SignatureKeyPair = signatureKeyPair; + PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair)); + SecurityState = securityState; + } +} diff --git a/src/Core/KeyManagement/Models/Api/Response/PublicKeyEncryptionKeyPairResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/PublicKeyEncryptionKeyPairResponseModel.cs new file mode 100644 index 0000000000..e5436b6131 --- /dev/null +++ b/src/Core/KeyManagement/Models/Api/Response/PublicKeyEncryptionKeyPairResponseModel.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Core.KeyManagement.Models.Api.Response; + + +public class PublicKeyEncryptionKeyPairResponseModel : ResponseModel +{ + [JsonPropertyName("wrappedPrivateKey")] + public required string WrappedPrivateKey { get; set; } + [JsonPropertyName("publicKey")] + public required string PublicKey { get; set; } + [JsonPropertyName("signedPublicKey")] + public string? SignedPublicKey { get; set; } + + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public PublicKeyEncryptionKeyPairResponseModel(PublicKeyEncryptionKeyPairData keyPair) + : base("publicKeyEncryptionKeyPair") + { + WrappedPrivateKey = keyPair.WrappedPrivateKey; + PublicKey = keyPair.PublicKey; + SignedPublicKey = keyPair.SignedPublicKey; + } + + [JsonConstructor] + public PublicKeyEncryptionKeyPairResponseModel(string wrappedPrivateKey, string publicKey, string? signedPublicKey) + : base("publicKeyEncryptionKeyPair") + { + WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey)); + PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey)); + SignedPublicKey = signedPublicKey; + } +} diff --git a/src/Core/KeyManagement/Models/Api/Response/PublicKeysResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/PublicKeysResponseModel.cs new file mode 100644 index 0000000000..b341a87e3e --- /dev/null +++ b/src/Core/KeyManagement/Models/Api/Response/PublicKeysResponseModel.cs @@ -0,0 +1,30 @@ +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Core.KeyManagement.Models.Api.Response; + + +/// +/// This response model is used to return the public keys of a user, to any other registered user or entity on the server. +/// It can contain public keys (signature/encryption), and proofs between the two. It does not contain (encrypted) private keys. +/// +public class PublicKeysResponseModel : ResponseModel +{ + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public PublicKeysResponseModel(UserAccountKeysData accountKeys) + : base("publicKeys") + { + ArgumentNullException.ThrowIfNull(accountKeys); + PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey; + + if (accountKeys.SignatureKeyPairData != null) + { + SignedPublicKey = accountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey; + VerifyingKey = accountKeys.SignatureKeyPairData.VerifyingKey; + } + } + + public string? VerifyingKey { get; set; } + public string? SignedPublicKey { get; set; } + public required string PublicKey { get; set; } +} diff --git a/src/Core/KeyManagement/Models/Api/Response/SignatureKeyPairResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/SignatureKeyPairResponseModel.cs new file mode 100644 index 0000000000..34d51f8bd4 --- /dev/null +++ b/src/Core/KeyManagement/Models/Api/Response/SignatureKeyPairResponseModel.cs @@ -0,0 +1,32 @@ +using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Core.KeyManagement.Models.Api.Response; + + +public class SignatureKeyPairResponseModel : ResponseModel +{ + [JsonPropertyName("wrappedSigningKey")] + public required string WrappedSigningKey { get; set; } + [JsonPropertyName("verifyingKey")] + public required string VerifyingKey { get; set; } + + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair) + : base("signatureKeyPair") + { + ArgumentNullException.ThrowIfNull(signatureKeyPair); + WrappedSigningKey = signatureKeyPair.WrappedSigningKey; + VerifyingKey = signatureKeyPair.VerifyingKey; + } + + + [JsonConstructor] + public SignatureKeyPairResponseModel(string wrappedSigningKey, string verifyingKey) + : base("signatureKeyPair") + { + WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey)); + VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey)); + } +} diff --git a/src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs b/src/Core/KeyManagement/Models/Api/Response/UserDecryptionResponseModel.cs similarity index 82% rename from src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs rename to src/Core/KeyManagement/Models/Api/Response/UserDecryptionResponseModel.cs index a4d259a00a..536347cea9 100644 --- a/src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs +++ b/src/Core/KeyManagement/Models/Api/Response/UserDecryptionResponseModel.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.KeyManagement.Models.Response; +namespace Bit.Core.KeyManagement.Models.Api.Response; public class UserDecryptionResponseModel { diff --git a/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs b/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs new file mode 100644 index 0000000000..fb8b09d390 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Bit.Core.KeyManagement.Models.Data; + + +public class PublicKeyEncryptionKeyPairData +{ + public required string WrappedPrivateKey { get; set; } + public string? SignedPublicKey { get; set; } + public required string PublicKey { get; set; } + + [JsonConstructor] + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public PublicKeyEncryptionKeyPairData(string wrappedPrivateKey, string publicKey, string? signedPublicKey = null) + { + WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey)); + PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey)); + SignedPublicKey = signedPublicKey; + } +} diff --git a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs index 557fb56ff3..19d14b273f 100644 --- a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs @@ -1,6 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - + using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; @@ -12,21 +10,19 @@ namespace Bit.Core.KeyManagement.Models.Data; public class RotateUserAccountKeysData { // Authentication for this requests - public string OldMasterKeyAuthenticationHash { get; set; } + public required string OldMasterKeyAuthenticationHash { get; set; } - // Other keys encrypted by the userkey - public string UserKeyEncryptedAccountPrivateKey { get; set; } - public string AccountPublicKey { get; set; } + public required UserAccountKeysData AccountKeys { get; set; } // All methods to get to the userkey - public MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; } - public IEnumerable EmergencyAccesses { get; set; } - public IReadOnlyList OrganizationUsers { get; set; } - public IEnumerable WebAuthnKeys { get; set; } - public IEnumerable DeviceKeys { get; set; } + public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; } + public required IEnumerable EmergencyAccesses { get; set; } + public required IReadOnlyList OrganizationUsers { get; set; } + public required IEnumerable WebAuthnKeys { get; set; } + public required IEnumerable DeviceKeys { get; set; } // User vault data encrypted by the userkey - public IEnumerable Ciphers { get; set; } - public IEnumerable Folders { get; set; } - public IReadOnlyList Sends { get; set; } + public required IEnumerable Ciphers { get; set; } + public required IEnumerable Folders { get; set; } + public required IReadOnlyList Sends { get; set; } } diff --git a/src/Core/KeyManagement/Models/Data/SecurityStateData.cs b/src/Core/KeyManagement/Models/Data/SecurityStateData.cs new file mode 100644 index 0000000000..c9a4610387 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/SecurityStateData.cs @@ -0,0 +1,10 @@ + +namespace Bit.Core.KeyManagement.Models.Data; + +public class SecurityStateData +{ + public required string SecurityState { get; set; } + // The security version is included in the security state, but needs COSE parsing, + // so this is a separate copy that can be used directly. + public required int SecurityVersion { get; set; } +} diff --git a/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs b/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs new file mode 100644 index 0000000000..32ae3eef8f --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs @@ -0,0 +1,21 @@ + +using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Enums; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class SignatureKeyPairData +{ + public required SignatureAlgorithm SignatureAlgorithm { get; set; } + public required string WrappedSigningKey { get; set; } + public required string VerifyingKey { get; set; } + + [JsonConstructor] + [System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute] + public SignatureKeyPairData(SignatureAlgorithm signatureAlgorithm, string wrappedSigningKey, string verifyingKey) + { + SignatureAlgorithm = signatureAlgorithm; + WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey)); + VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey)); + } +} diff --git a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs new file mode 100644 index 0000000000..cabdca59ea --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.KeyManagement.Models.Data; + + +public class UserAccountKeysData +{ + public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; } + public SignatureKeyPairData? SignatureKeyPairData { get; set; } + public SecurityStateData? SecurityStateData { get; set; } +} diff --git a/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs b/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs new file mode 100644 index 0000000000..4ea9b7582b --- /dev/null +++ b/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs @@ -0,0 +1,10 @@ + +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Queries.Interfaces; + +public interface IUserAccountKeysQuery +{ + Task Run(User user); +} diff --git a/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs b/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs new file mode 100644 index 0000000000..7aafd2cf1e --- /dev/null +++ b/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs @@ -0,0 +1,35 @@ + +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; +using Bit.Core.KeyManagement.Repositories; + +namespace Bit.Core.KeyManagement.Queries; + + +public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery +{ + public async Task Run(User user) + { + if (user.GetSecurityVersion() < 2) + { + return new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + }; + } + else + { + return new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id), + SecurityStateData = new SecurityStateData + { + SecurityState = user.SecurityState!, + SecurityVersion = user.GetSecurityVersion(), + } + }; + } + } +} diff --git a/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs b/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs new file mode 100644 index 0000000000..ce8979620f --- /dev/null +++ b/src/Core/KeyManagement/Repositories/IUserSignatureKeyPairRepository.cs @@ -0,0 +1,14 @@ + +using Bit.Core.KeyManagement.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Repositories; + +namespace Bit.Core.KeyManagement.Repositories; + +public interface IUserSignatureKeyPairRepository : IRepository +{ + public Task GetByUserIdAsync(Guid userId); + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signatureKeyPair); + public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signatureKeyPair); +} diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs index 91363abee8..c1e7905d78 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -1,6 +1,11 @@ -using Bit.Core.Auth.Repositories; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Repositories; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -25,6 +30,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IPasswordHasher _passwordHasher; + private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository; + private readonly IFeatureService _featureService; /// /// Instantiates a new @@ -36,16 +43,19 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand /// Provides a method to update re-encrypted send data /// Provides a method to update re-encrypted emergency access data /// Provides a method to update re-encrypted organization user data + /// Provides a method to update re-encrypted device keys /// Hashes the new master password /// Logs out user from other devices after successful rotation /// Provides a password mismatch error if master password hash validation fails /// Provides a method to update re-encrypted WebAuthn keys + /// Provides a method to update re-encrypted signature keys public RotateUserAccountKeysCommand(IUserService userService, IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, IDeviceRepository deviceRepository, IPasswordHasher passwordHasher, IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository, + IUserSignatureKeyPairRepository userSignatureKeyPairRepository, IFeatureService featureService) { _userService = userService; @@ -60,6 +70,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand _identityErrorDescriber = errors; _credentialRepository = credentialRepository; _passwordHasher = passwordHasher; + _userSignatureKeyPairRepository = userSignatureKeyPairRepository; + _featureService = featureService; } /// @@ -80,50 +92,106 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand user.LastKeyRotationDate = now; user.SecurityStamp = Guid.NewGuid().ToString(); - if ( - !model.MasterPasswordUnlockData.ValidateForUser(user) - ) + List saveEncryptedDataActions = []; + + await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions); + UpdateUnlockMethods(model, user, saveEncryptedDataActions); + UpdateUserData(model, user, saveEncryptedDataActions); + + await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions); + await _pushService.PushLogOutAsync(user.Id); + return IdentityResult.Success; + } + + public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + { + ValidateV2Encryption(model); + await ValidateVerifyingKeyUnchangedAsync(model, user); + + saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.UpdateForKeyRotation(user.Id, model.AccountKeys.SignatureKeyPairData)); + user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey; + user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState; + user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion; + } + + public void UpgradeV1ToV2Keys(RotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + { + ValidateV2Encryption(model); + saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData)); + user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey; + user.SecurityState = model.AccountKeys.SecurityStateData!.SecurityState; + user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion; + } + + public async Task UpdateAccountKeysAsync(RotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + { + ValidatePublicKeyEncryptionKeyPairUnchanged(model, user); + + if (IsV2EncryptionUserAsync(user)) { - throw new InvalidOperationException("The provided master password unlock data is not valid for this user."); + await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions); } - if ( - model.AccountPublicKey != user.PublicKey - ) + else if (model.AccountKeys.SignatureKeyPairData != null) { - throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation."); + UpgradeV1ToV2Keys(model, user, saveEncryptedDataActions); + } + else + { + if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64) + { + throw new InvalidOperationException("The provided account private key was not wrapped with AES-256-CBC-HMAC"); + } + // V1 user to V1 user rotation needs to further changes, the private key was re-encrypted. } - user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey; - user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey; - user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash); - user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint; + // Private key is re-wrapped with new user key by client + user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey; + } + + public void UpdateUserData(RotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + { + // The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data + // with an old-user key-encrypted copy + var now = DateTime.UtcNow; - List saveEncryptedDataActions = new(); if (model.Ciphers.Any()) { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); + var ciphersWithUpdatedDate = model.Ciphers.ToList().Select(c => { c.RevisionDate = now; return c; }); + saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, ciphersWithUpdatedDate)); } if (model.Folders.Any()) { - saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders)); + var foldersWithUpdatedDate = model.Folders.ToList().Select(f => { f.RevisionDate = now; return f; }); + saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, foldersWithUpdatedDate)); } if (model.Sends.Any()) { - saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends)); + var sendsWithUpdatedDate = model.Sends.ToList().Select(s => { s.RevisionDate = now; return s; }); + saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, sendsWithUpdatedDate)); } + } + + void UpdateUnlockMethods(RotateUserAccountKeysData model, User user, List saveEncryptedDataActions) + { + if (!model.MasterPasswordUnlockData.ValidateForUser(user)) + { + throw new InvalidOperationException("The provided master password unlock data is not valid for this user."); + } + // Update master password authentication & unlock + user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey; + user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash); + user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint; if (model.EmergencyAccesses.Any()) { - saveEncryptedDataActions.Add( - _emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses)); + saveEncryptedDataActions.Add(_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses)); } if (model.OrganizationUsers.Any()) { - saveEncryptedDataActions.Add( - _organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers)); + saveEncryptedDataActions.Add(_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers)); } if (model.WebAuthnKeys.Any()) @@ -135,9 +203,80 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand { saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys)); } + } - await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions); - await _pushService.PushLogOutAsync(user.Id); - return IdentityResult.Success; + private bool IsV2EncryptionUserAsync(User user) + { + // Returns whether the user is a V2 user based on the private key's encryption type. + ArgumentNullException.ThrowIfNull(user); + var isPrivateKeyEncryptionV2 = GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64; + return isPrivateKeyEncryptionV2; + } + + private async Task ValidateVerifyingKeyUnchangedAsync(RotateUserAccountKeysData model, User user) + { + var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id) ?? throw new InvalidOperationException("User does not have a signature key pair."); + if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair!.VerifyingKey) + { + throw new InvalidOperationException("The provided verifying key does not match the user's current verifying key."); + } + } + + private static void ValidatePublicKeyEncryptionKeyPairUnchanged(RotateUserAccountKeysData model, User user) + { + var publicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey; + if (publicKey != user.PublicKey) + { + throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric key pair is currently not supported during key rotation."); + } + } + + private static void ValidateV2Encryption(RotateUserAccountKeysData model) + { + if (model.AccountKeys.SignatureKeyPairData == null) + { + throw new InvalidOperationException("Signature key pair data is required for V2 encryption."); + } + if (GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64) + { + throw new InvalidOperationException("The provided signing key data is not wrapped with XChaCha20-Poly1305."); + } + if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.VerifyingKey)) + { + throw new InvalidOperationException("The provided signature key pair data does not contain a valid verifying key."); + } + + if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64) + { + throw new InvalidOperationException("The provided private key encryption key is not wrapped with XChaCha20-Poly1305."); + } + if (string.IsNullOrEmpty(model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey)) + { + throw new InvalidOperationException("No signed public key provided, but the user already has a signature key pair."); + } + if (model.AccountKeys.SecurityStateData == null || string.IsNullOrEmpty(model.AccountKeys.SecurityStateData.SecurityState)) + { + throw new InvalidOperationException("No signed security state provider for V2 user"); + } + } + + /// + /// Helper method to convert an encryption type string to an enum value. + /// + private static EncryptionType GetEncryptionType(string encString) + { + var parts = encString.Split('.'); + if (parts.Length == 1) + { + throw new ArgumentException("Invalid encryption type string."); + } + if (byte.TryParse(parts[0], out var encryptionTypeNumber)) + { + if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber)) + { + return (EncryptionType)encryptionTypeNumber; + } + } + throw new ArgumentException("Invalid encryption type string."); } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index e57ed1c85f..28d4630993 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -14,6 +14,8 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Api; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; @@ -45,6 +47,7 @@ public abstract class BaseRequestValidator where T : class protected IUserService _userService { get; } protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; } protected IPolicyRequirementQuery PolicyRequirementQuery { get; } + protected IUserAccountKeysQuery _accountKeysQuery { get; } public BaseRequestValidator( UserManager userManager, @@ -63,7 +66,8 @@ public abstract class BaseRequestValidator where T : class IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IPolicyRequirementQuery policyRequirementQuery, IAuthRequestRepository authRequestRepository, - IMailService mailService + IMailService mailService, + IUserAccountKeysQuery userAccountKeysQuery ) { _userManager = userManager; @@ -83,6 +87,7 @@ public abstract class BaseRequestValidator where T : class PolicyRequirementQuery = policyRequirementQuery; _authRequestRepository = authRequestRepository; _mailService = mailService; + _accountKeysQuery = userAccountKeysQuery; } protected async Task ValidateAsync(T context, ValidatedTokenRequest request, @@ -439,6 +444,8 @@ public abstract class BaseRequestValidator where T : class if (!string.IsNullOrWhiteSpace(user.PrivateKey)) { customResponse.Add("PrivateKey", user.PrivateKey); + var accountKeys = await _accountKeysQuery.Run(user); + customResponse.Add("AccountKeys", new PrivateKeysResponseModel(accountKeys)); } if (!string.IsNullOrWhiteSpace(user.Key)) diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 1495973b80..64156ea5f3 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -8,6 +8,7 @@ using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; @@ -47,7 +48,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs new file mode 100644 index 0000000000..5dcc2943b8 --- /dev/null +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -0,0 +1,79 @@ +using System.Data; +using Bit.Core.KeyManagement.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.KeyManagement.Repositories; + +public class UserSignatureKeyPairRepository : Repository, IUserSignatureKeyPairRepository +{ + public UserSignatureKeyPairRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + } + + public UserSignatureKeyPairRepository(string connectionString, string readOnlyConnectionString) : base( + connectionString, readOnlyConnectionString) + { + } + + public async Task GetByUserIdAsync(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + return (await connection.QuerySingleOrDefaultAsync( + "[dbo].[UserSignatureKeyPair_ReadByUserId]", + new + { + UserId = userId + }, + commandType: CommandType.StoredProcedure))?.ToSignatureKeyPairData(); + } + } + + public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + await connection.QueryAsync( + "[dbo].[UserSignatureKeyPair_SetForRotation]", + new + { + Id = CoreHelpers.GenerateComb(), + UserId = userId, + SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm, + SigningKey = signingKeys.WrappedSigningKey, + VerifyingKey = signingKeys.VerifyingKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow + }, + commandType: CommandType.StoredProcedure, + transaction: transaction); + }; + } + + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + await connection.QueryAsync( + "[dbo].[UserSignatureKeyPair_UpdateForRotation]", + new + { + UserId = grantorId, + SignatureAlgorithm = (byte)signingKeys.SignatureAlgorithm, + SigningKey = signingKeys.WrappedSigningKey, + VerifyingKey = signingKeys.VerifyingKey, + RevisionDate = DateTime.UtcNow + }, + commandType: CommandType.StoredProcedure, + transaction: transaction); + }; + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 7a6507230e..3c35df2a82 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -108,6 +108,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Configurations/UserSignatureKeyPairEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/KeyManagement/Configurations/UserSignatureKeyPairEntityTypeConfiguration.cs new file mode 100644 index 0000000000..aa10a73a88 --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Configurations/UserSignatureKeyPairEntityTypeConfiguration.cs @@ -0,0 +1,22 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Configurations; + +public class UserSignatureKeyPairEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(s => s.Id) + .ValueGeneratedNever(); + + builder + .HasIndex(s => s.UserId) + .IsUnique() + .IsClustered(false); + + builder.ToTable(nameof(UserSignatureKeyPair)); + } +} diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Models/UserSignatureKeyPair.cs b/src/Infrastructure.EntityFramework/KeyManagement/Models/UserSignatureKeyPair.cs new file mode 100644 index 0000000000..b2bd8a1345 --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Models/UserSignatureKeyPair.cs @@ -0,0 +1,19 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Models; + +public class UserSignatureKeyPair : Core.KeyManagement.Entities.UserSignatureKeyPair +{ + public virtual User User { get; set; } +} + +public class UserSignatureKeyPairMapperProfile : Profile +{ + public UserSignatureKeyPairMapperProfile() + { + CreateMap().ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs new file mode 100644 index 0000000000..04f055501d --- /dev/null +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -0,0 +1,66 @@ + +using AutoMapper; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; +using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Utilities; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.KeyManagement.Repositories; + +public class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : Repository(serviceScopeFactory, mapper, context => context.UserSignatureKeyPairs), IUserSignatureKeyPairRepository +{ + public async Task GetByUserIdAsync(Guid userId) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var signingKeys = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == userId); + if (signingKeys == null) + { + return null; + } + + return signingKeys.ToSignatureKeyPairData(); + } + + public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys) + { + return async (_, _) => + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var entity = new Models.UserSignatureKeyPair + { + Id = CoreHelpers.GenerateComb(), + UserId = userId, + SignatureAlgorithm = signingKeys.SignatureAlgorithm, + SigningKey = signingKeys.WrappedSigningKey, + VerifyingKey = signingKeys.VerifyingKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + }; + await dbContext.UserSignatureKeyPairs.AddAsync(entity); + await dbContext.SaveChangesAsync(); + }; + } + + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid grantorId, SignatureKeyPairData signingKeys) + { + return async (_, _) => + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var entity = await dbContext.UserSignatureKeyPairs.FirstOrDefaultAsync(x => x.UserId == grantorId); + if (entity != null) + { + entity.SignatureAlgorithm = signingKeys.SignatureAlgorithm; + entity.SigningKey = signingKeys.WrappedSigningKey; + entity.VerifyingKey = signingKeys.VerifyingKey; + entity.RevisionDate = DateTime.UtcNow; + await dbContext.SaveChangesAsync(); + } + }; + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index 0b3393ca78..b748a26db2 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -74,6 +74,7 @@ public class DatabaseContext : DbContext public DbSet TaxRates { get; set; } public DbSet Transactions { get; set; } public DbSet Users { get; set; } + public DbSet UserSignatureKeyPairs { get; set; } public DbSet AuthRequests { get; set; } public DbSet OrganizationDomains { get; set; } public DbSet WebAuthnCredentials { get; set; } diff --git a/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql new file mode 100644 index 0000000000..8bfa0156af --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[UserSignatureKeyPairView] + WHERE + [UserId] = @UserId; +END diff --git a/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql new file mode 100644 index 0000000000..6ee33e2a40 --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[UserSignatureKeyPair_SetForRotation] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @SignatureAlgorithm TINYINT, + @SigningKey VARCHAR(MAX), + @VerifyingKey VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[UserSignatureKeyPair] + ( + [Id], + [UserId], + [SignatureAlgorithm], + [SigningKey], + [VerifyingKey], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @UserId, + @SignatureAlgorithm, + @SigningKey, + @VerifyingKey, + @CreationDate, + @RevisionDate + ) +END diff --git a/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql new file mode 100644 index 0000000000..4f673019fc --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[UserSignatureKeyPair_UpdateForRotation] + @UserId UNIQUEIDENTIFIER, + @SignatureAlgorithm TINYINT, + @SigningKey VARCHAR(MAX), + @VerifyingKey VARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + UPDATE + [dbo].[UserSignatureKeyPair] + SET + [SignatureAlgorithm] = @SignatureAlgorithm, + [SigningKey] = @SigningKey, + [VerifyingKey] = @VerifyingKey, + [RevisionDate] = @RevisionDate + WHERE + [UserId] = @UserId; +END diff --git a/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql b/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql new file mode 100644 index 0000000000..94d4e48a0b --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql @@ -0,0 +1,16 @@ +CREATE TABLE [dbo].[UserSignatureKeyPair] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [SignatureAlgorithm] TINYINT NOT NULL, + [SigningKey] VARCHAR(MAX) NOT NULL, + [VerifyingKey] VARCHAR(MAX) NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE +); +GO + +CREATE UNIQUE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId] + ON [dbo].[UserSignatureKeyPair]([UserId] ASC); +GO diff --git a/src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql b/src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql new file mode 100644 index 0000000000..959305a3e7 --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[UserSignatureKeyPairView] +AS +SELECT + * +FROM + [dbo].[UserSignatureKeyPair] diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql index 60d9b5eb32..2573bf1a0a 100644 --- a/src/Sql/dbo/Stored Procedures/User_Create.sql +++ b/src/Sql/dbo/Stored Procedures/User_Create.sql @@ -41,7 +41,10 @@ @LastKdfChangeDate DATETIME2(7) = NULL, @LastKeyRotationDate DATETIME2(7) = NULL, @LastEmailChangeDate DATETIME2(7) = NULL, - @VerifyDevices BIT = 1 + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL AS BEGIN SET NOCOUNT ON @@ -90,7 +93,10 @@ BEGIN [LastKdfChangeDate], [LastKeyRotationDate], [LastEmailChangeDate], - [VerifyDevices] + [VerifyDevices], + [SecurityState], + [SecurityVersion], + [SignedPublicKey] ) VALUES ( @@ -136,6 +142,9 @@ BEGIN @LastKdfChangeDate, @LastKeyRotationDate, @LastEmailChangeDate, - @VerifyDevices + @VerifyDevices, + @SecurityState, + @SecurityVersion, + @SignedPublicKey ) END diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index 15d04d72f6..5097bc538e 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -41,7 +41,10 @@ @LastKdfChangeDate DATETIME2(7) = NULL, @LastKeyRotationDate DATETIME2(7) = NULL, @LastEmailChangeDate DATETIME2(7) = NULL, - @VerifyDevices BIT = 1 + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL AS BEGIN SET NOCOUNT ON @@ -90,7 +93,10 @@ BEGIN [LastKdfChangeDate] = @LastKdfChangeDate, [LastKeyRotationDate] = @LastKeyRotationDate, [LastEmailChangeDate] = @LastEmailChangeDate, - [VerifyDevices] = @VerifyDevices + [VerifyDevices] = @VerifyDevices, + [SecurityState] = @SecurityState, + [SecurityVersion] = @SecurityVersion, + [SignedPublicKey] = @SignedPublicKey WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index 239ee67f11..dc772ff1a7 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -42,6 +42,9 @@ [LastKeyRotationDate] DATETIME2 (7) NULL, [LastEmailChangeDate] DATETIME2 (7) NULL, [VerifyDevices] BIT DEFAULT ((1)) NOT NULL, + [SecurityState] VARCHAR (MAX) NULL, + [SecurityVersion] INT NULL, + [SignedPublicKey] VARCHAR (MAX) NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index bf27d7f0d1..1630bc0dc0 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -12,6 +12,10 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Entities; +using Bit.Core.KeyManagement.Enums; +using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.Repositories; using Bit.Core.Vault.Enums; using Bit.Test.Common.AutoFixture.Attributes; @@ -24,6 +28,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture _passwordHasher; private readonly IOrganizationRepository _organizationRepository; + private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository; private string _ownerEmail = null!; public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) @@ -49,6 +55,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture(); _passwordHasher = _factory.GetService>(); _organizationRepository = _factory.GetService(); + _userSignatureKeyPairRepository = _factory.GetService(); } public async Task InitializeAsync() @@ -200,6 +207,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture(); @@ -48,6 +49,7 @@ public class AccountsControllerTests : IDisposable _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); + _userAccountKeysQuery = Substitute.For(); _twoFactorEmailService = Substitute.For(); _changeKdfCommand = Substitute.For(); @@ -61,6 +63,7 @@ public class AccountsControllerTests : IDisposable _tdeOffboardingPasswordCommand, _twoFactorIsEnabledQuery, _featureService, + _userAccountKeysQuery, _twoFactorEmailService, _changeKdfCommand ); diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 05b1aa5a4d..2e41dd79a0 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -110,6 +110,7 @@ public class AccountsKeyManagementControllerTests public async Task RotateUserAccountKeysSuccess(SutProvider sutProvider, RotateUserAccountKeysAndDataRequestModel data, User user) { + data.AccountKeys.SignatureKeyPair = null; sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); sutProvider.GetDependency().RotateUserAccountKeysAsync(Arg.Any(), Arg.Any()) .Returns(IdentityResult.Success); @@ -142,8 +143,60 @@ public class AccountsKeyManagementControllerTests && d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash && d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey - && d.AccountPublicKey == data.AccountKeys.AccountPublicKey - && d.UserKeyEncryptedAccountPrivateKey == data.AccountKeys.UserKeyEncryptedAccountPrivateKey + && d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey + && d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey + )); + } + + [Theory] + [BitAutoData] + public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider sutProvider, + RotateUserAccountKeysAndDataRequestModel data, User user) + { + data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel + { + SignatureAlgorithm = "ed25519", + WrappedSigningKey = "wrappedSigningKey", + VerifyingKey = "verifyingKey" + }; + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency().RotateUserAccountKeysAsync(Arg.Any(), Arg.Any()) + .Returns(IdentityResult.Success); + await sutProvider.Sut.RotateUserAccountKeysAsync(data); + + await sutProvider.GetDependency, IEnumerable>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData)); + await sutProvider.GetDependency, IReadOnlyList>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountUnlockData.OrganizationAccountRecoveryUnlockData)); + await sutProvider.GetDependency, IEnumerable>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountUnlockData.PasskeyUnlockData)); + + await sutProvider.GetDependency, IEnumerable>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountData.Ciphers)); + await sutProvider.GetDependency, IEnumerable>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountData.Folders)); + await sutProvider.GetDependency, IReadOnlyList>>().Received(1) + .ValidateAsync(Arg.Any(), Arg.Is(data.AccountData.Sends)); + + await sutProvider.GetDependency().Received(1) + .RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is(d => + d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash + + && d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType + && d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations + && d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory + && d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism + && d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email + + && d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash + && d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey + + && d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey + && d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey + && d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey + && d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519 + && d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey + && d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey )); } @@ -153,6 +206,7 @@ public class AccountsKeyManagementControllerTests public async Task RotateUserKeyNoUser_Throws(SutProvider sutProvider, RotateUserAccountKeysAndDataRequestModel data) { + data.AccountKeys.SignatureKeyPair = null; User? user = null; sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); sutProvider.GetDependency().RotateUserAccountKeysAsync(Arg.Any(), Arg.Any()) @@ -165,6 +219,7 @@ public class AccountsKeyManagementControllerTests public async Task RotateUserKeyWrongData_Throws(SutProvider sutProvider, RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber) { + data.AccountKeys.SignatureKeyPair = null; sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).Returns(user); sutProvider.GetDependency().RotateUserAccountKeysAsync(Arg.Any(), Arg.Any()) .Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch())); diff --git a/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs new file mode 100644 index 0000000000..6e3094234b --- /dev/null +++ b/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs @@ -0,0 +1,112 @@ +#nullable enable +using Bit.Api.KeyManagement.Controllers; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Enums; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Controllers; + +[ControllerCustomize(typeof(UsersController))] +[SutProviderCustomize] +[JsonDocumentCustomize] +public class UsersControllerTests +{ + [Theory] + [BitAutoData] + public async Task GetPublicKey_NotFound_ThrowsNotFoundException( + SutProvider sutProvider) + { + sutProvider.GetDependency().GetPublicKeyAsync(Arg.Any()).ReturnsNull(); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetPublicKeyAsync(new Guid())); + } + + [Theory] + [BitAutoData] + public async Task GetPublicKey_ReturnsUserKeyResponseModel( + SutProvider sutProvider, + Guid userId) + { + var publicKey = "publicKey"; + sutProvider.GetDependency().GetPublicKeyAsync(userId).Returns(publicKey); + + var result = await sutProvider.Sut.GetPublicKeyAsync(userId); + Assert.NotNull(result); + Assert.Equal(userId, result.UserId); + Assert.Equal(publicKey, result.PublicKey); + } + + [Theory] + [BitAutoData] + public async Task GetAccountKeys_UserNotFound_ThrowsNotFoundException( + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).ReturnsNull(); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAccountKeysAsync(new Guid())); + } + + [Theory] + [BitAutoData] + public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel( + SutProvider sutProvider, + Guid userId) + { + var user = new User + { + Id = userId, + PublicKey = "publicKey", + SignedPublicKey = "signedPublicKey", + }; + + sutProvider.GetDependency().GetByIdAsync(userId).Returns(user); + sutProvider.GetDependency() + .Run(user) + .Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", "signedPublicKey"), + SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"), + }); + + var result = await sutProvider.Sut.GetAccountKeysAsync(userId); + Assert.NotNull(result); + Assert.Equal("publicKey", result.PublicKey); + Assert.Equal("signedPublicKey", result.SignedPublicKey); + Assert.Equal("verifyingKey", result.VerifyingKey); + } + + [Theory] + [BitAutoData] + public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel_WithNullVerifyingKey( + SutProvider sutProvider, + Guid userId) + { + var user = new User + { + Id = userId, + PublicKey = "publicKey", + SignedPublicKey = null, + }; + + sutProvider.GetDependency().GetByIdAsync(userId).Returns(user); + sutProvider.GetDependency() + .Run(user) + .Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", null), + SignatureKeyPairData = null, + }); + + var result = await sutProvider.Sut.GetAccountKeysAsync(userId); + Assert.NotNull(result); + Assert.Equal("publicKey", result.PublicKey); + Assert.Null(result.SignedPublicKey); + Assert.Null(result.VerifyingKey); + } +} diff --git a/test/Api.Test/KeyManagement/Models/Request/SignatureKeyPairRequestModel.cs b/test/Api.Test/KeyManagement/Models/Request/SignatureKeyPairRequestModel.cs new file mode 100644 index 0000000000..704371eebd --- /dev/null +++ b/test/Api.Test/KeyManagement/Models/Request/SignatureKeyPairRequestModel.cs @@ -0,0 +1,22 @@ +#nullable enable + +using Bit.Api.KeyManagement.Models.Requests; +using Xunit; + +namespace Bit.Api.Test.KeyManagement.Models.Request; + +public class SignatureKeyPairRequestModelTests +{ + [Fact] + public void ToSignatureKeyPairData_WrongAlgorithm_Rejects() + { + var model = new SignatureKeyPairRequestModel + { + SignatureAlgorithm = "abc", + WrappedSigningKey = "wrappedKey", + VerifyingKey = "verifyingKey" + }; + + Assert.Throws(() => model.ToSignatureKeyPairData()); + } +} diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 54db1e4053..aaee8e0c2c 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -12,6 +12,8 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; @@ -74,6 +76,7 @@ public class SyncControllerTests var policyRepository = sutProvider.GetDependency(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // Adjust random data to match required formats / test intentions user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains); @@ -98,6 +101,11 @@ public class SyncControllerTests // Setup returns userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); organizationUserRepository .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails); @@ -127,7 +135,6 @@ public class SyncControllerTests // Execute GET var result = await sutProvider.Sut.Get(); - // Asserts // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); @@ -166,6 +173,7 @@ public class SyncControllerTests var policyRepository = sutProvider.GetDependency(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // Adjust random data to match required formats / test intentions user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains); @@ -189,6 +197,11 @@ public class SyncControllerTests // Setup returns userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); organizationUserRepository .GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails); @@ -256,6 +269,7 @@ public class SyncControllerTests var policyRepository = sutProvider.GetDependency(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // Adjust random data to match required formats / test intentions user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains); @@ -290,6 +304,12 @@ public class SyncControllerTests twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + // Execute GET var result = await sutProvider.Sut.Get(); @@ -327,6 +347,13 @@ public class SyncControllerTests user.MasterPassword = null; + var userAccountKeysQuery = sutProvider.GetDependency(); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + var userService = sutProvider.GetDependency(); userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); @@ -352,6 +379,13 @@ public class SyncControllerTests user.KdfMemory = kdfMemory; user.KdfParallelism = kdfParallelism; + var userAccountKeysQuery = sutProvider.GetDependency(); + userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = null, + }); + var userService = sutProvider.GetDependency(); userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); diff --git a/test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs b/test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs new file mode 100644 index 0000000000..f79217acba --- /dev/null +++ b/test/Core.Test/KeyManagement/Queries/UserAccountKeysQuery.cs @@ -0,0 +1,43 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries; +using Bit.Core.KeyManagement.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Queries; + +[SutProviderCustomize] +public class UserAccountKeysQueryTests +{ + [Theory, BitAutoData] + public async Task V1User_Success(SutProvider sutProvider, User user) + { + var result = await sutProvider.Sut.Run(user); + Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey); + Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey); + } + + [Theory, BitAutoData] + public async Task V2User_Success(SutProvider sutProvider, User user) + { + user.SecurityState = "v2"; + user.SecurityVersion = 2; + var signatureKeyPairRepository = sutProvider.GetDependency(); + signatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey")); + var result = await sutProvider.Sut.Run(user); + Assert.Equal(user.GetPublicKeyEncryptionKeyPair().PublicKey, result.PublicKeyEncryptionKeyPairData.PublicKey); + Assert.Equal(user.GetPublicKeyEncryptionKeyPair().WrappedPrivateKey, result.PublicKeyEncryptionKeyPairData.WrappedPrivateKey); + Assert.Equal(user.GetPublicKeyEncryptionKeyPair().SignedPublicKey, result.PublicKeyEncryptionKeyPairData.SignedPublicKey); + + Assert.NotNull(result.SignatureKeyPairData); + Assert.Equal("wrappedSigningKey", result.SignatureKeyPairData.WrappedSigningKey); + Assert.Equal("verifyingKey", result.SignatureKeyPairData.VerifyingKey); + + Assert.Equal(user.SecurityState, result.SecurityStateData.SecurityState); + Assert.Equal(user.GetSecurityVersion(), result.SecurityStateData.SecurityVersion); + } + +} diff --git a/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs index e677814fc1..f4d1fc5c94 100644 --- a/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs +++ b/test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs @@ -1,11 +1,18 @@ using Bit.Core.Entities; +using Bit.Core.KeyManagement.Enums; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.KeyManagement.UserKey.Implementations; using Bit.Core.Services; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Repositories; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Core.Test.KeyManagement.UserKey; @@ -14,7 +21,7 @@ namespace Bit.Core.Test.KeyManagement.UserKey; public class RotateUserAccountKeysCommandTests { [Theory, BitAutoData] - public async Task RejectsWrongOldMasterPassword(SutProvider sutProvider, User user, + public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) { user.Email = model.MasterPasswordUnlockData.Email; @@ -25,41 +32,38 @@ public class RotateUserAccountKeysCommandTests Assert.NotEqual(IdentityResult.Success, result); } + [Theory, BitAutoData] - public async Task ThrowsWhenUserIsNull(SutProvider sutProvider, + public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider sutProvider, RotateUserAccountKeysData model) { await Assert.ThrowsAsync(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model)); } + [Theory, BitAutoData] - public async Task RejectsEmailChange(SutProvider sutProvider, User user, + public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) { - user.Kdf = Enums.KdfType.Argon2id; - user.KdfIterations = 3; - user.KdfMemory = 64; - user.KdfParallelism = 4; + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); model.MasterPasswordUnlockData.Email = user.Email + ".different-domain"; - model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; - model.MasterPasswordUnlockData.KdfIterations = 3; - model.MasterPasswordUnlockData.KdfMemory = 64; - model.MasterPasswordUnlockData.KdfParallelism = 4; sutProvider.GetDependency().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) .Returns(true); await Assert.ThrowsAsync(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model)); } [Theory, BitAutoData] - public async Task RejectsKdfChange(SutProvider sutProvider, User user, + public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) { - user.Kdf = Enums.KdfType.Argon2id; - user.KdfIterations = 3; - user.KdfMemory = 64; - user.KdfParallelism = 4; + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); - model.MasterPasswordUnlockData.Email = user.Email; model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256; model.MasterPasswordUnlockData.KdfIterations = 600000; model.MasterPasswordUnlockData.KdfMemory = null; @@ -71,22 +75,15 @@ public class RotateUserAccountKeysCommandTests [Theory, BitAutoData] - public async Task RejectsPublicKeyChange(SutProvider sutProvider, User user, + public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) { - user.PublicKey = "old-public"; - user.Kdf = Enums.KdfType.Argon2id; - user.KdfIterations = 3; - user.KdfMemory = 64; - user.KdfParallelism = 4; - - model.AccountPublicKey = "new-public"; - model.MasterPasswordUnlockData.Email = user.Email; - model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; - model.MasterPasswordUnlockData.KdfIterations = 3; - model.MasterPasswordUnlockData.KdfMemory = 64; - model.MasterPasswordUnlockData.KdfParallelism = 4; + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public"; sutProvider.GetDependency().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) .Returns(true); @@ -94,27 +91,350 @@ public class RotateUserAccountKeysCommandTests } [Theory, BitAutoData] - public async Task RotatesCorrectly(SutProvider sutProvider, User user, + public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider sutProvider, User user, RotateUserAccountKeysData model) { - user.Kdf = Enums.KdfType.Argon2id; - user.KdfIterations = 3; - user.KdfMemory = 64; - user.KdfParallelism = 4; - - model.MasterPasswordUnlockData.Email = user.Email; - model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; - model.MasterPasswordUnlockData.KdfIterations = 3; - model.MasterPasswordUnlockData.KdfMemory = 64; - model.MasterPasswordUnlockData.KdfParallelism = 4; - - model.AccountPublicKey = user.PublicKey; + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); sutProvider.GetDependency().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) .Returns(true); var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model); - Assert.Equal(IdentityResult.Success, result); } + + [Theory, BitAutoData] + public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider sutProvider, User user, + RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + + sutProvider.GetDependency().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) + .Returns(true); + + var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model); + Assert.Equal(IdentityResult.Success, result); + Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState); + } + + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + + model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public"; + var saveEncryptedDataActions = new List(); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.xxx"; + + var saveEncryptedDataActions = new List(); + await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "7.xxx"; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V1_Success(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + + var saveEncryptedDataActions = new List(); + await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions); + Assert.Empty(saveEncryptedDataActions); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V2_Success(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + + var saveEncryptedDataActions = new List(); + await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions); + Assert.NotEmpty(saveEncryptedDataActions); + Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState); + } + + + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key"; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = "2.xxx"; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.SignatureKeyPairData.VerifyingKey = ""; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "2.abc"; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.SecurityStateData = null; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("No signed security state provider for V2 user", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV2ExistingUser(user, signatureRepository); + SetV2ModelUser(model); + model.AccountKeys.SignatureKeyPairData = null; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = ""; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("Invalid encryption type string.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + SetTestKdfAndSaltForUserAndModel(user, model); + var signatureRepository = sutProvider.GetDependency(); + SetV1ExistingUser(user, signatureRepository); + SetV1ModelUser(model); + model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx"; + + var saveEncryptedDataActions = new List(); + var ex = await Assert.ThrowsAsync(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions)); + Assert.Equal("Invalid encryption type string.", ex.Message); + } + + [Theory, BitAutoData] + public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider sutProvider, User user, RotateUserAccountKeysData model) + { + var oldDate = new DateTime(2017, 1, 1); + + var cipher = Substitute.For(); + cipher.RevisionDate = oldDate; + model.Ciphers = [cipher]; + + var folder = Substitute.For(); + folder.RevisionDate = oldDate; + model.Folders = [folder]; + + var send = Substitute.For(); + send.RevisionDate = oldDate; + model.Sends = [send]; + + var saveEncryptedDataActions = new List(); + + sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions); + foreach (var dataAction in saveEncryptedDataActions) + { + await dataAction.Invoke(); + } + + var updatedCiphers = sutProvider.GetDependency() + .ReceivedCalls() + .FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? + .GetArguments()[1] as IEnumerable; + foreach (var updatedCipher in updatedCiphers!) + { + var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id); + Assert.NotEqual(oldDate, updatedCipher.RevisionDate); + } + + var updatedFolders = sutProvider.GetDependency() + .ReceivedCalls() + .FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? + .GetArguments()[1] as IEnumerable; + foreach (var updatedFolder in updatedFolders!) + { + var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id); + Assert.NotEqual(oldDate, updatedFolder.RevisionDate); + } + + var updatedSends = sutProvider.GetDependency() + .ReceivedCalls() + .FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? + .GetArguments()[1] as IEnumerable; + foreach (var updatedSend in updatedSends!) + { + var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id); + Assert.NotEqual(oldDate, updatedSend.RevisionDate); + } + } + + // Helper functions to set valid test parameters that match each other to the model and user. + private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model) + { + user.Kdf = Enums.KdfType.Argon2id; + user.KdfIterations = 3; + user.KdfMemory = 64; + user.KdfParallelism = 4; + model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id; + model.MasterPasswordUnlockData.KdfIterations = 3; + model.MasterPasswordUnlockData.KdfMemory = 64; + model.MasterPasswordUnlockData.KdfParallelism = 4; + // The email is the salt for the KDF and is validated currently. + user.Email = model.MasterPasswordUnlockData.Email; + } + + private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository) + { + user.PrivateKey = "2.abc"; + user.PublicKey = "public"; + user.SignedPublicKey = null; + userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull(); + } + + private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository) + { + user.PrivateKey = "7.abc"; + user.PublicKey = "public"; + user.SignedPublicKey = "signed-public"; + userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key")); + } + + private static void SetV1ModelUser(RotateUserAccountKeysData model) + { + model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("2.abc", "public", null); + model.AccountKeys.SignatureKeyPairData = null; + model.AccountKeys.SecurityStateData = null; + } + + private static void SetV2ModelUser(RotateUserAccountKeysData model) + { + model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("7.abc", "public", "signed-public"); + model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "7.abc", "verifying-key"); + model.AccountKeys.SecurityStateData = new SecurityStateData + { + SecurityState = "abc", + SecurityVersion = 2, + }; + } } diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 5a4f83818a..6e7c327621 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -10,7 +10,9 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.KeyManagement.Models.Response; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Api; using Bit.Core.Repositories; using Bit.Core.Services; @@ -51,6 +53,7 @@ public class BaseRequestValidatorTests private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IAuthRequestRepository _authRequestRepository; private readonly IMailService _mailService; + private readonly IUserAccountKeysQuery _userAccountKeysQuery; private readonly BaseRequestValidatorTestWrapper _sut; @@ -73,6 +76,7 @@ public class BaseRequestValidatorTests _policyRequirementQuery = Substitute.For(); _authRequestRepository = Substitute.For(); _mailService = Substitute.For(); + _userAccountKeysQuery = Substitute.For(); _sut = new BaseRequestValidatorTestWrapper( _userManager, @@ -91,7 +95,8 @@ public class BaseRequestValidatorTests _userDecryptionOptionsBuilder, _policyRequirementQuery, _authRequestRepository, - _mailService); + _mailService, + _userAccountKeysQuery); } /* Logic path @@ -180,6 +185,13 @@ public class BaseRequestValidatorTests // 5 -> not legacy user _userService.IsLegacyUser(Arg.Any()) .Returns(false); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); // Act await _sut.ValidateAsync(context); @@ -227,6 +239,13 @@ public class BaseRequestValidatorTests // 5 -> not legacy user _userService.IsLegacyUser(Arg.Any()) .Returns(false); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); // Act await _sut.ValidateAsync(context); @@ -460,6 +479,13 @@ public class BaseRequestValidatorTests .Returns(Task.FromResult(new Tuple(false, null))); _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) .Returns(Task.FromResult(true)); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); await _sut.ValidateAsync(context); @@ -495,6 +521,13 @@ public class BaseRequestValidatorTests _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) .Returns(Task.FromResult(true)); context.ValidatedTokenRequest.ClientId = "web"; + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); // Act await _sut.ValidateAsync(context); @@ -529,6 +562,13 @@ public class BaseRequestValidatorTests _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) .Returns(Task.FromResult(true)); context.ValidatedTokenRequest.ClientId = "web"; + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); // Act await _sut.ValidateAsync(context); @@ -591,6 +631,13 @@ public class BaseRequestValidatorTests HasMasterPassword = false, MasterPasswordUnlock = null })); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); var context = CreateContext(tokenRequest, requestContext, grantResult); _sut.isValid = true; @@ -644,6 +691,14 @@ public class BaseRequestValidatorTests } })); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); + var context = CreateContext(tokenRequest, requestContext, grantResult); _sut.isValid = true; @@ -671,6 +726,152 @@ public class BaseRequestValidatorTests Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt); } + [Theory, BitAutoData] + public async Task ValidateAsync_CustomResponse_ShouldIncludeAccountKeys( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var mockAccountKeys = new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key", + "test-signed-public-key" + ), + SignatureKeyPairData = new SignatureKeyPairData( + Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519, + "test-wrapped-signing-key", + "test-verifying-key" + ), + SecurityStateData = new SecurityStateData + { + SecurityState = "test-security-state", + SecurityVersion = 2 + } + }; + + _userAccountKeysQuery.Run(Arg.Any()).Returns(mockAccountKeys); + + _userDecryptionOptionsBuilder.ForUser(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithDevice(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithSso(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions + { + HasMasterPassword = true, + MasterPasswordUnlock = new MasterPasswordUnlockResponseModel + { + Kdf = new MasterPasswordUnlockKdfResponseModel + { + KdfType = KdfType.PBKDF2_SHA256, + Iterations = 100000 + }, + MasterKeyEncryptedUserKey = _mockEncryptedString, + Salt = "test@example.com" + } + })); + + var context = CreateContext(tokenRequest, requestContext, grantResult); + _sut.isValid = true; + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + var customResponse = context.GrantResult.CustomResponse; + + // Verify AccountKeys are included in response + Assert.Contains("AccountKeys", customResponse); + Assert.IsType(customResponse["AccountKeys"]); + + var accountKeysResponse = (PrivateKeysResponseModel)customResponse["AccountKeys"]; + Assert.NotNull(accountKeysResponse.PublicKeyEncryptionKeyPair); + Assert.Equal("test-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.PublicKey); + Assert.Equal("test-private-key", accountKeysResponse.PublicKeyEncryptionKeyPair.WrappedPrivateKey); + Assert.Equal("test-signed-public-key", accountKeysResponse.PublicKeyEncryptionKeyPair.SignedPublicKey); + + Assert.NotNull(accountKeysResponse.SignatureKeyPair); + Assert.Equal("test-wrapped-signing-key", accountKeysResponse.SignatureKeyPair.WrappedSigningKey); + Assert.Equal("test-verifying-key", accountKeysResponse.SignatureKeyPair.VerifyingKey); + + Assert.NotNull(accountKeysResponse.SecurityState); + Assert.Equal("test-security-state", accountKeysResponse.SecurityState.SecurityState); + Assert.Equal(2, accountKeysResponse.SecurityState.SecurityVersion); + } + [Theory, BitAutoData] + public async Task ValidateAsync_CustomResponse_AccountKeysQuery_SkippedWhenPrivateKeyIsNull( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + requestContext.User.PrivateKey = null; + + var context = CreateContext(tokenRequest, requestContext, grantResult); + _sut.isValid = true; + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + + // Verify that the account keys query wasn't called. + await _userAccountKeysQuery.Received(0).Run(Arg.Any()); + } + [Theory, BitAutoData] + public async Task ValidateAsync_CustomResponse_AccountKeysQuery_CalledWithCorrectUser( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var expectedUser = requestContext.User; + + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + "test-private-key", + "test-public-key" + ) + }); + + _userDecryptionOptionsBuilder.ForUser(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithDevice(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithSso(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.WithWebAuthnLoginCredential(Arg.Any()).Returns(_userDecryptionOptionsBuilder); + _userDecryptionOptionsBuilder.BuildAsync().Returns(Task.FromResult(new UserDecryptionOptions())); + + var context = CreateContext(tokenRequest, requestContext, grantResult); + _sut.isValid = true; + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + + // Verify that the account keys query was called with the correct user + await _userAccountKeysQuery.Received(1).Run(Arg.Is(u => u.Id == expectedUser.Id)); + } + private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index db3deedf02..ec3e791d5b 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -64,7 +65,8 @@ IBaseRequestValidatorTestWrapper IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IPolicyRequirementQuery policyRequirementQuery, IAuthRequestRepository authRequestRepository, - IMailService mailService) : + IMailService mailService, + IUserAccountKeysQuery userAccountKeysQuery) : base( userManager, userService, @@ -82,7 +84,8 @@ IBaseRequestValidatorTestWrapper userDecryptionOptionsBuilder, policyRequirementQuery, authRequestRepository, - mailService) + mailService, + userAccountKeysQuery) { } diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 5c7b3ed99d..4e43894a06 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -92,6 +92,7 @@ public class EfRepositoryListBuilder : ISpecimenBuilder where T : BaseEntityF cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); cfg.AddProfile(); }) .CreateMapper())); diff --git a/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/UserCompare.cs b/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/UserCompare.cs index 90a6af51bd..c49b37705c 100644 --- a/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/UserCompare.cs +++ b/test/Infrastructure.EFIntegration.Test/Repositories/EqualityComparers/UserCompare.cs @@ -29,7 +29,8 @@ public class UserCompare : IEqualityComparer x.LicenseKey == y.LicenseKey && x.ApiKey == y.ApiKey && x.Kdf == y.Kdf && - x.KdfIterations == y.KdfIterations; + x.KdfIterations == y.KdfIterations && + x.SignedPublicKey == y.SignedPublicKey; } public int GetHashCode([DisallowNull] User obj) diff --git a/util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql b/util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql new file mode 100644 index 0000000000..8a5373be11 --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-13_00_UserCryptoV2.sql @@ -0,0 +1,396 @@ +IF OBJECT_ID('[dbo].[UserSignatureKeyPair]') IS NULL +BEGIN + CREATE TABLE [dbo].[UserSignatureKeyPair] + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [SignatureAlgorithm] TINYINT NOT NULL, + [SigningKey] VARCHAR(MAX) NOT NULL, + [VerifyingKey] VARCHAR(MAX) NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE + ); +END +GO + +IF NOT EXISTS(SELECT name +FROM sys.indexes +WHERE name = 'IX_UserSignatureKeyPair_UserId') +BEGIN +CREATE UNIQUE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId] + ON [dbo].[UserSignatureKeyPair]([UserId] ASC); +END +GO + + +CREATE OR ALTER VIEW [dbo].[UserSignatureKeyPairView] +AS +SELECT + * +FROM + [dbo].[UserSignatureKeyPair] +GO + +CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_ReadByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[UserSignatureKeyPairView] + WHERE + [UserId] = @UserId; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_UpdateForRotation] + @UserId UNIQUEIDENTIFIER, + @SignatureAlgorithm TINYINT, + @SigningKey VARCHAR(MAX), + @VerifyingKey VARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + UPDATE + [dbo].[UserSignatureKeyPair] + SET + [SignatureAlgorithm] = @SignatureAlgorithm, + [SigningKey] = @SigningKey, + [VerifyingKey] = @VerifyingKey, + [RevisionDate] = @RevisionDate + WHERE + [UserId] = @UserId; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[UserSignatureKeyPair_SetForRotation] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @SignatureAlgorithm TINYINT, + @SigningKey VARCHAR(MAX), + @VerifyingKey VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[UserSignatureKeyPair] + ( + [Id], + [UserId], + [SignatureAlgorithm], + [SigningKey], + [VerifyingKey], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @UserId, + @SignatureAlgorithm, + @SigningKey, + @VerifyingKey, + @CreationDate, + @RevisionDate + ) +END +GO + +IF COL_LENGTH('[dbo].[User]', 'SecurityState') IS NULL +BEGIN + ALTER TABLE + [dbo].[User] + ADD + [SecurityState] VARCHAR(MAX) NULL; +END +GO + +IF COL_LENGTH('[dbo].[User]', 'SecurityVersion') IS NULL +BEGIN + ALTER TABLE + [dbo].[User] + ADD + [SecurityVersion] INT NULL; +END +GO + +IF COL_LENGTH('[dbo].[User]', 'SignedPublicKey') IS NULL +BEGIN + ALTER TABLE + [dbo].[User] + ADD + [SignedPublicKey] VARCHAR(MAX) NULL; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[User_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Name NVARCHAR(50), + @Email NVARCHAR(256), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT = NULL, + @KdfParallelism INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30), + @ForcePasswordReset BIT = 0, + @UsesKeyConnector BIT = 0, + @FailedLoginCount INT = 0, + @LastFailedLoginDate DATETIME2(7), + @AvatarColor VARCHAR(7) = NULL, + @LastPasswordChangeDate DATETIME2(7) = NULL, + @LastKdfChangeDate DATETIME2(7) = NULL, + @LastKeyRotationDate DATETIME2(7) = NULL, + @LastEmailChangeDate DATETIME2(7) = NULL, + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[User] + ( + [Id], + [Name], + [Email], + [EmailVerified], + [MasterPassword], + [MasterPasswordHint], + [Culture], + [SecurityStamp], + [TwoFactorProviders], + [TwoFactorRecoveryCode], + [EquivalentDomains], + [ExcludedGlobalEquivalentDomains], + [AccountRevisionDate], + [Key], + [PublicKey], + [PrivateKey], + [Premium], + [PremiumExpirationDate], + [RenewalReminderDate], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [LicenseKey], + [Kdf], + [KdfIterations], + [CreationDate], + [RevisionDate], + [ApiKey], + [ForcePasswordReset], + [UsesKeyConnector], + [FailedLoginCount], + [LastFailedLoginDate], + [AvatarColor], + [KdfMemory], + [KdfParallelism], + [LastPasswordChangeDate], + [LastKdfChangeDate], + [LastKeyRotationDate], + [LastEmailChangeDate], + [VerifyDevices], + [SecurityState], + [SecurityVersion], + [SignedPublicKey] + ) + VALUES + ( + @Id, + @Name, + @Email, + @EmailVerified, + @MasterPassword, + @MasterPasswordHint, + @Culture, + @SecurityStamp, + @TwoFactorProviders, + @TwoFactorRecoveryCode, + @EquivalentDomains, + @ExcludedGlobalEquivalentDomains, + @AccountRevisionDate, + @Key, + @PublicKey, + @PrivateKey, + @Premium, + @PremiumExpirationDate, + @RenewalReminderDate, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @LicenseKey, + @Kdf, + @KdfIterations, + @CreationDate, + @RevisionDate, + @ApiKey, + @ForcePasswordReset, + @UsesKeyConnector, + @FailedLoginCount, + @LastFailedLoginDate, + @AvatarColor, + @KdfMemory, + @KdfParallelism, + @LastPasswordChangeDate, + @LastKdfChangeDate, + @LastKeyRotationDate, + @LastEmailChangeDate, + @VerifyDevices, + @SecurityState, + @SecurityVersion, + @SignedPublicKey + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[User_Update] + @Id UNIQUEIDENTIFIER, + @Name NVARCHAR(50), + @Email NVARCHAR(256), + @EmailVerified BIT, + @MasterPassword NVARCHAR(300), + @MasterPasswordHint NVARCHAR(50), + @Culture NVARCHAR(10), + @SecurityStamp NVARCHAR(50), + @TwoFactorProviders NVARCHAR(MAX), + @TwoFactorRecoveryCode NVARCHAR(32), + @EquivalentDomains NVARCHAR(MAX), + @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), + @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), + @PublicKey NVARCHAR(MAX), + @PrivateKey NVARCHAR(MAX), + @Premium BIT, + @PremiumExpirationDate DATETIME2(7), + @RenewalReminderDate DATETIME2(7), + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @LicenseKey VARCHAR(100), + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT = NULL, + @KdfParallelism INT = NULL, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ApiKey VARCHAR(30), + @ForcePasswordReset BIT = 0, + @UsesKeyConnector BIT = 0, + @FailedLoginCount INT, + @LastFailedLoginDate DATETIME2(7), + @AvatarColor VARCHAR(7), + @LastPasswordChangeDate DATETIME2(7) = NULL, + @LastKdfChangeDate DATETIME2(7) = NULL, + @LastKeyRotationDate DATETIME2(7) = NULL, + @LastEmailChangeDate DATETIME2(7) = NULL, + @VerifyDevices BIT = 1, + @SecurityState VARCHAR(MAX) = NULL, + @SecurityVersion INT = NULL, + @SignedPublicKey VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Name] = @Name, + [Email] = @Email, + [EmailVerified] = @EmailVerified, + [MasterPassword] = @MasterPassword, + [MasterPasswordHint] = @MasterPasswordHint, + [Culture] = @Culture, + [SecurityStamp] = @SecurityStamp, + [TwoFactorProviders] = @TwoFactorProviders, + [TwoFactorRecoveryCode] = @TwoFactorRecoveryCode, + [EquivalentDomains] = @EquivalentDomains, + [ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains, + [AccountRevisionDate] = @AccountRevisionDate, + [Key] = @Key, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [Premium] = @Premium, + [PremiumExpirationDate] = @PremiumExpirationDate, + [RenewalReminderDate] = @RenewalReminderDate, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [LicenseKey] = @LicenseKey, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [KdfMemory] = @KdfMemory, + [KdfParallelism] = @KdfParallelism, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ApiKey] = @ApiKey, + [ForcePasswordReset] = @ForcePasswordReset, + [UsesKeyConnector] = @UsesKeyConnector, + [FailedLoginCount] = @FailedLoginCount, + [LastFailedLoginDate] = @LastFailedLoginDate, + [AvatarColor] = @AvatarColor, + [LastPasswordChangeDate] = @LastPasswordChangeDate, + [LastKdfChangeDate] = @LastKdfChangeDate, + [LastKeyRotationDate] = @LastKeyRotationDate, + [LastEmailChangeDate] = @LastEmailChangeDate, + [VerifyDevices] = @VerifyDevices, + [SecurityState] = @SecurityState, + [SecurityVersion] = @SecurityVersion, + [SignedPublicKey] = @SignedPublicKey + WHERE + [Id] = @Id +END +GO + +EXECUTE sp_refreshview 'dbo.UserView' +EXECUTE sp_refreshview 'dbo.EmergencyAccessDetailsView' +EXECUTE sp_refreshview 'dbo.OrganizationUserUserDetailsView' +EXECUTE sp_refreshview 'dbo.ProviderUserUserDetailsView' +EXECUTE sp_refreshview 'dbo.UserEmailDomainView' +GO \ No newline at end of file diff --git a/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs b/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs new file mode 100644 index 0000000000..a0aa7ef682 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.Designer.cs @@ -0,0 +1,3338 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251013083703_UserCryptoV2")] + partial class UserCryptoV2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ArchivedDate") + .HasColumnType("datetime(6)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.cs b/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.cs new file mode 100644 index 0000000000..9811e401dd --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251013083703_UserCryptoV2.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class UserCryptoV2 : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SecurityState", + table: "User", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "SecurityVersion", + table: "User", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignedPublicKey", + table: "User", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "UserSignatureKeyPair", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + SignatureAlgorithm = table.Column(type: "tinyint unsigned", nullable: false), + VerifyingKey = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SigningKey = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id); + table.ForeignKey( + name: "FK_UserSignatureKeyPair_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_UserSignatureKeyPair_UserId", + table: "UserSignatureKeyPair", + column: "UserId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserSignatureKeyPair"); + + migrationBuilder.DropColumn( + name: "SecurityState", + table: "User"); + + migrationBuilder.DropColumn( + name: "SecurityVersion", + table: "User"); + + migrationBuilder.DropColumn( + name: "SignedPublicKey", + table: "User"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 842ce555e5..5f3ac17713 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1873,6 +1873,15 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(50) .HasColumnType("varchar(50)"); + b.Property("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + b.Property("Storage") .HasColumnType("bigint"); @@ -1901,6 +1910,40 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("User", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.Property("Id") @@ -2948,6 +2991,17 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs b/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs new file mode 100644 index 0000000000..0fc48fc5e5 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.Designer.cs @@ -0,0 +1,3344 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251013083502_UserCryptoV2")] + partial class UserCryptoV2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ArchivedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.cs b/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.cs new file mode 100644 index 0000000000..3e9ddfe831 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251013083502_UserCryptoV2.cs @@ -0,0 +1,79 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class UserCryptoV2 : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SecurityState", + table: "User", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SecurityVersion", + table: "User", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignedPublicKey", + table: "User", + type: "text", + nullable: true); + + migrationBuilder.CreateTable( + name: "UserSignatureKeyPair", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + SignatureAlgorithm = table.Column(type: "smallint", nullable: false), + VerifyingKey = table.Column(type: "text", nullable: false), + SigningKey = table.Column(type: "text", nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id); + table.ForeignKey( + name: "FK_UserSignatureKeyPair_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserSignatureKeyPair_UserId", + table: "UserSignatureKeyPair", + column: "UserId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserSignatureKeyPair"); + + migrationBuilder.DropColumn( + name: "SecurityState", + table: "User"); + + migrationBuilder.DropColumn( + name: "SecurityVersion", + table: "User"); + + migrationBuilder.DropColumn( + name: "SignedPublicKey", + table: "User"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 2279ae43af..f5af466cc8 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1879,6 +1879,15 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + b.Property("Storage") .HasColumnType("bigint"); @@ -1907,6 +1916,40 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("User", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.Property("Id") @@ -2954,6 +2997,17 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") diff --git a/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs b/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs new file mode 100644 index 0000000000..3ec01ef19f --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.Designer.cs @@ -0,0 +1,3327 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251013083514_UserCryptoV2")] + partial class UserCryptoV2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ArchivedDate") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.cs b/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.cs new file mode 100644 index 0000000000..d6c09d3a13 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251013083514_UserCryptoV2.cs @@ -0,0 +1,79 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class UserCryptoV2 : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SecurityState", + table: "User", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "SecurityVersion", + table: "User", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "SignedPublicKey", + table: "User", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "UserSignatureKeyPair", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + SignatureAlgorithm = table.Column(type: "INTEGER", nullable: false), + VerifyingKey = table.Column(type: "TEXT", nullable: false), + SigningKey = table.Column(type: "TEXT", nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserSignatureKeyPair", x => x.Id); + table.ForeignKey( + name: "FK_UserSignatureKeyPair_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserSignatureKeyPair_UserId", + table: "UserSignatureKeyPair", + column: "UserId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserSignatureKeyPair"); + + migrationBuilder.DropColumn( + name: "SecurityState", + table: "User"); + + migrationBuilder.DropColumn( + name: "SecurityVersion", + table: "User"); + + migrationBuilder.DropColumn( + name: "SignedPublicKey", + table: "User"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index dc66860e6c..d6b2a90b96 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1862,6 +1862,15 @@ namespace Bit.SqliteMigrations.Migrations .HasMaxLength(50) .HasColumnType("TEXT"); + b.Property("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + b.Property("Storage") .HasColumnType("INTEGER"); @@ -1890,6 +1899,40 @@ namespace Bit.SqliteMigrations.Migrations b.ToTable("User", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.Property("Id") @@ -2937,6 +2980,17 @@ namespace Bit.SqliteMigrations.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => { b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") From dbce45291ce0a2e96f35009f2bc8a0815bcbcfbc Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 20 Oct 2025 07:27:18 -0500 Subject: [PATCH 31/52] [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. --- .../Controllers/OrganizationsController.cs | 1 + .../Models/OrganizationEditModel.cs | 5 + .../Views/Shared/_OrganizationForm.cshtml | 7 + .../OrganizationResponseModel.cs | 2 + .../ProfileOrganizationResponseModel.cs | 3 + ...rofileProviderOrganizationResponseModel.cs | 1 + .../AdminConsole/Entities/Organization.cs | 5 + .../Data/Organizations/OrganizationAbility.cs | 2 + .../OrganizationUserOrganizationDetails.cs | 1 + .../ProviderUserOrganizationDetails.cs | 1 + .../Services/OrganizationFactory.cs | 1 + src/Core/Billing/Licenses/LicenseConstants.cs | 1 + .../OrganizationLicenseClaimsFactory.cs | 1 + src/Core/Billing/Models/StaticStore/Plan.cs | 2 + .../Models/OrganizationLicense.cs | 8 +- src/Core/Constants.cs | 1 + .../Repositories/OrganizationRepository.cs | 8 +- ...roviderUserOrganizationDetailsViewQuery.cs | 1 + .../Stored Procedures/Organization_Create.sql | 131 +- .../Organization_ReadAbilities.sql | 3 +- .../Stored Procedures/Organization_Update.sql | 8 +- src/Sql/dbo/Tables/Organization.sql | 1 + ...rganizationUserOrganizationDetailsView.sql | 3 +- ...derUserProviderOrganizationDetailsView.sql | 3 +- .../OrganizationsControllerTests.cs | 37 + .../UpdateOrganizationLicenseCommandTests.cs | 2 +- .../2025-10-07_00_AddAutoConfirmUser.sql | 509 +++ ...142240_AddAutoConfirmUserToOrg.Designer.cs | 3287 ++++++++++++++++ .../20251010142240_AddAutoConfirmUserToOrg.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...142249_AddAutoConfirmUserToOrg.Designer.cs | 3293 +++++++++++++++++ .../20251010142249_AddAutoConfirmUserToOrg.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...142244_AddAutoConfirmUserToOrg.Designer.cs | 3276 ++++++++++++++++ .../20251010142244_AddAutoConfirmUserToOrg.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + 36 files changed, 10618 insertions(+), 79 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-10-07_00_AddAutoConfirmUser.sql create mode 100644 util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.cs create mode 100644 util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.cs create mode 100644 util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 2417bf610d..0d992cb96a 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -472,6 +472,7 @@ public class OrganizationsController : Controller organization.UseRiskInsights = model.UseRiskInsights; organization.UseOrganizationDomains = model.UseOrganizationDomains; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; + organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation; //secrets organization.SmSeats = model.SmSeats; diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index b64af3135f..6059a003b6 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -106,6 +106,8 @@ public class OrganizationEditModel : OrganizationViewModel SmServiceAccounts = org.SmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; UseOrganizationDomains = org.UseOrganizationDomains; + UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation; + _plans = plans; } @@ -192,6 +194,8 @@ public class OrganizationEditModel : OrganizationViewModel [Display(Name = "Use Organization Domains")] public bool UseOrganizationDomains { 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 +235,7 @@ public class OrganizationEditModel : OrganizationViewModel LegacyYear = p.LegacyYear, Disabled = p.Disabled, SupportsSecretsManager = p.SupportsSecretsManager, + AutomaticUserConfirmation = p.AutomaticUserConfirmation, PasswordManager = new { diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index 267264a38f..06ae5b03b3 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -159,6 +159,13 @@ } + @if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)) + { +
+ + +
+ }

Password Manager

diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index b34765fb19..8006a85734 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -70,6 +70,7 @@ public class OrganizationResponseModel : ResponseModel UseRiskInsights = organization.UseRiskInsights; UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; + UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; } public Guid Id { get; set; } @@ -118,6 +119,7 @@ public class OrganizationResponseModel : ResponseModel public bool UseRiskInsights { get; set; } public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } } public class OrganizationSubscriptionResponseModel : OrganizationResponseModel diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index fd2bfe06dc..5a8669bb52 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -87,6 +87,8 @@ public class ProfileOrganizationResponseModel : ResponseModel KeyConnectorUrl = ssoConfigData.KeyConnectorUrl; SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType; } + + UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; } public Guid Id { get; set; } @@ -164,4 +166,5 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool IsAdminInitiated { get; set; } public bool SsoEnabled { get; set; } public MemberDecryptionType? SsoMemberDecryptionType { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 24b6fed704..fcbb949757 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -52,5 +52,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo UseRiskInsights = organization.UseRiskInsights; UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; + UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 7933990e74..4cbde4a61a 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -129,6 +129,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable ///
public bool SyncSeats { get; set; } + /// + /// If set to true, user accounts created within the organization are automatically confirmed without requiring additional verification steps. + /// + public bool UseAutomaticUserConfirmation { get; set; } + public void SetNewId() { if (Id == default(Guid)) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index ae91f204e3..3c02a4f50b 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -28,6 +28,7 @@ public class OrganizationAbility UseRiskInsights = organization.UseRiskInsights; UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; + UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation; } public Guid Id { get; set; } @@ -49,4 +50,5 @@ public class OrganizationAbility public bool UseRiskInsights { get; set; } public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index b7e573c4e6..04e481d340 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -66,4 +66,5 @@ public class OrganizationUserOrganizationDetails public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool? IsAdminInitiated { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index 04281d098e..7d68f685b8 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -51,4 +51,5 @@ public class ProviderUserOrganizationDetails public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public ProviderType ProviderType { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } } diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs index afb3931ec4..42d6e7c8d5 100644 --- a/src/Core/AdminConsole/Services/OrganizationFactory.cs +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -61,6 +61,7 @@ public static class OrganizationFactory claimsPrincipal.GetValue(OrganizationLicenseConstants.UseOrganizationDomains), UseAdminSponsoredFamilies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAdminSponsoredFamilies), + UseAutomaticUserConfirmation = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseAutomaticUserConfirmation), }; public static Organization Create( diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index cdfac76614..79ac94be62 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -43,6 +43,7 @@ public static class OrganizationLicenseConstants public const string Trial = nameof(Trial); public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies); public const string UseOrganizationDomains = nameof(UseOrganizationDomains); + public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation); } public static class UserLicenseConstants diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 1e049d7f03..e9aadbe758 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -56,6 +56,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory SecretsManager != null; + public bool AutomaticUserConfirmation { get; init; } + public bool HasNonSeatBasedPasswordManagerPlan() => PasswordManager is { StripePlanId: not null and not "", StripeSeatPlanId: null or "" }; diff --git a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs index 83789be2f3..7ccbacc938 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs @@ -153,6 +153,7 @@ public class OrganizationLicense : ILicense public LicenseType? LicenseType { get; set; } public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool UseAutomaticUserConfirmation { get; set; } public string Hash { get; set; } public string Signature { get; set; } public string Token { get; set; } @@ -226,7 +227,8 @@ public class OrganizationLicense : ILicense // any new fields added need to be added here so that they're ignored !p.Name.Equals(nameof(UseRiskInsights)) && !p.Name.Equals(nameof(UseAdminSponsoredFamilies)) && - !p.Name.Equals(nameof(UseOrganizationDomains))) + !p.Name.Equals(nameof(UseOrganizationDomains)) && + !p.Name.Equals(nameof(UseAutomaticUserConfirmation))) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); @@ -421,6 +423,7 @@ public class OrganizationLicense : ILicense var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(UseAdminSponsoredFamilies)); var useOrganizationDomains = claimsPrincipal.GetValue(nameof(UseOrganizationDomains)); + var useAutomaticUserConfirmation = claimsPrincipal.GetValue(nameof(UseAutomaticUserConfirmation)); return issued <= DateTime.UtcNow && expires >= DateTime.UtcNow && @@ -450,7 +453,8 @@ public class OrganizationLicense : ILicense smSeats == organization.SmSeats && smServiceAccounts == organization.SmServiceAccounts && useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies && - useOrganizationDomains == organization.UseOrganizationDomains; + useOrganizationDomains == organization.UseOrganizationDomains && + useAutomaticUserConfirmation == organization.UseAutomaticUserConfirmation; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0cf4a4040e..3bdd884377 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -140,6 +140,7 @@ public static class FeatureFlagKeys public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; + public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; /* Auth Team */ diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 200c4aa308..2238bfca76 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -112,7 +112,8 @@ public class OrganizationRepository : Repository GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs index 3689ae4d18..f7f3e70d33 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs @@ -52,6 +52,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery sutProvider) + { + // Arrange + var update = new OrganizationEditModel + { + PlanType = PlanType.TeamsMonthly, + UseAutomaticUserConfirmation = true + }; + + organization.UseAutomaticUserConfirmation = false; + + sutProvider.GetDependency() + .UserHasPermission(Permission.Org_Plan_Edit) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + + organizationRepository.GetByIdAsync(organization.Id).Returns(organization); + + // Act + _ = await sutProvider.Sut.Edit(organization.Id, update); + + // Assert + await organizationRepository.Received(1).ReplaceAsync(Arg.Is(o => o.Id == organization.Id + && o.UseAutomaticUserConfirmation == true)); + + // Annul + await organizationRepository.DeleteAsync(organization); + } + #endregion } diff --git a/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs index 8570dfc6be..ea76f9d975 100644 --- a/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs @@ -88,7 +88,7 @@ public class UpdateOrganizationLicenseCommandTests "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion", "LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems", - "UseOrganizationDomains", "UseAdminSponsoredFamilies") && + "UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation") && // Same property but different name, use explicit mapping org.ExpirationDate == license.Expires)); } diff --git a/util/Migrator/DbScripts/2025-10-07_00_AddAutoConfirmUser.sql b/util/Migrator/DbScripts/2025-10-07_00_AddAutoConfirmUser.sql new file mode 100644 index 0000000000..db6e73e979 --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-07_00_AddAutoConfirmUser.sql @@ -0,0 +1,509 @@ +-- Add AutomaticallyConfirmUsers column to Organization table +IF COL_LENGTH('[dbo].[Organization]', 'UseAutomaticUserConfirmation') IS NULL + BEGIN + ALTER TABLE [dbo].[Organization] + ADD [UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0); + END +GO + +-- Update Organization_Create stored procedure +CREATE OR ALTER PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT= null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = NULL, + @LimitCollectionDeletion BIT = NULL, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0, + @UseAutomaticUserConfirmation BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector], + [UseScim], + [UseCustomPermissions], + [UseSecretsManager], + [Status], + [UsePasswordManager], + [SmSeats], + [SmServiceAccounts], + [MaxAutoscaleSmSeats], + [MaxAutoscaleSmServiceAccounts], + [SecretsManagerBeta], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies], + [SyncSeats], + [UseAutomaticUserConfirmation] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector, + @UseScim, + @UseCustomPermissions, + @UseSecretsManager, + @Status, + @UsePasswordManager, + @SmSeats, + @SmServiceAccounts, + @MaxAutoscaleSmSeats, + @MaxAutoscaleSmServiceAccounts, + @SecretsManagerBeta, + @LimitCollectionCreation, + @LimitCollectionDeletion, + @AllowAdminAccessToAllCollectionItems, + @UseRiskInsights, + @LimitItemDeletion, + @UseOrganizationDomains, + @UseAdminSponsoredFamilies, + @SyncSeats, + @UseAutomaticUserConfirmation + ); +END +GO + +-- Update Organization_Update stored procedure +CREATE OR ALTER PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0, + @UseScim BIT = 0, + @UseCustomPermissions BIT = 0, + @UseSecretsManager BIT = 0, + @Status TINYINT = 0, + @UsePasswordManager BIT = 1, + @SmSeats INT = null, + @SmServiceAccounts INT = null, + @MaxAutoscaleSmSeats INT = null, + @MaxAutoscaleSmServiceAccounts INT = null, + @SecretsManagerBeta BIT = 0, + @LimitCollectionCreation BIT = null, + @LimitCollectionDeletion BIT = null, + @AllowAdminAccessToAllCollectionItems BIT = 0, + @UseRiskInsights BIT = 0, + @LimitItemDeletion BIT = 0, + @UseOrganizationDomains BIT = 0, + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0, + @UseAutomaticUserConfirmation BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector, + [UseScim] = @UseScim, + [UseCustomPermissions] = @UseCustomPermissions, + [UseSecretsManager] = @UseSecretsManager, + [Status] = @Status, + [UsePasswordManager] = @UsePasswordManager, + [SmSeats] = @SmSeats, + [SmServiceAccounts] = @SmServiceAccounts, + [MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats, + [MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts, + [SecretsManagerBeta] = @SecretsManagerBeta, + [LimitCollectionCreation] = @LimitCollectionCreation, + [LimitCollectionDeletion] = @LimitCollectionDeletion, + [AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems, + [UseRiskInsights] = @UseRiskInsights, + [LimitItemDeletion] = @LimitItemDeletion, + [UseOrganizationDomains] = @UseOrganizationDomains, + [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies, + [SyncSeats] = @SyncSeats, + [UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation + WHERE + [Id] = @Id; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseCustomPermissions], + [UseSso], + [UseKeyConnector], + [UseScim], + [UseResetPassword], + [UsePolicies], + [Enabled], + [LimitCollectionCreation], + [LimitCollectionDeletion], + [AllowAdminAccessToAllCollectionItems], + [UseRiskInsights], + [LimitItemDeletion], + [UseOrganizationDomains], + [UseAdminSponsoredFamilies], + [UseAutomaticUserConfirmation] + FROM + [dbo].[Organization] +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + P.[Type] ProviderType, + SS.[Enabled] SsoEnabled, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[LimitItemDeletion], + O.[UseAdminSponsoredFamilies], + O.[UseOrganizationDomains], + OS.[IsAdminInitiated], + O.[UseAutomaticUserConfirmation] +FROM + [dbo].[OrganizationUser] OU + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] + LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] + LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] + +GO + +CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView] +AS +SELECT + PU.[UserId], + PO.[OrganizationId], + O.[Name], + O.[Enabled], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + PO.[Key], + O.[PublicKey], + O.[PrivateKey], + PU.[Status], + PU.[Type], + PO.[ProviderId], + PU.[Id] ProviderUserId, + P.[Name] ProviderName, + O.[PlanType], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + P.[Type] ProviderType, + O.[LimitItemDeletion], + O.[UseOrganizationDomains], + O.[UseAutomaticUserConfirmation] +FROM + [dbo].[ProviderUser] PU + INNER JOIN + [dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId] + INNER JOIN + [dbo].[Organization] O ON O.[Id] = PO.[OrganizationId] + INNER JOIN + [dbo].[Provider] P ON P.[Id] = PU.[ProviderId] +GO + +EXEC sp_refreshview '[dbo].[OrganizationView]'; +EXEC sp_refreshview '[dbo].[OrganizationCipherDetailsCollectionsView]'; +EXEC sp_refreshview '[dbo].[ProviderOrganizationOrganizationDetailsView]'; +GO diff --git a/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.Designer.cs b/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.Designer.cs new file mode 100644 index 0000000000..e6549a3b82 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.Designer.cs @@ -0,0 +1,3287 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251010142240_AddAutoConfirmUserToOrg")] + partial class AddAutoConfirmUserToOrg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("Filters") + .HasColumnType("longtext"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ArchivedDate") + .HasColumnType("datetime(6)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.cs b/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.cs new file mode 100644 index 0000000000..d0141d2b78 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20251010142240_AddAutoConfirmUserToOrg.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddAutoConfirmUserToOrg : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 5f3ac17713..78a9433c53 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -220,6 +220,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("UseApi") .HasColumnType("tinyint(1)"); + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + b.Property("UseCustomPermissions") .HasColumnType("tinyint(1)"); diff --git a/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.Designer.cs b/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.Designer.cs new file mode 100644 index 0000000000..71f1645b52 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.Designer.cs @@ -0,0 +1,3293 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251010142249_AddAutoConfirmUserToOrg")] + partial class AddAutoConfirmUserToOrg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Filters") + .HasColumnType("text"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ArchivedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.cs b/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.cs new file mode 100644 index 0000000000..f2b793951e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20251010142249_AddAutoConfirmUserToOrg.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddAutoConfirmUserToOrg : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index f5af466cc8..db34ccd7d0 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -222,6 +222,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("UseApi") .HasColumnType("boolean"); + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + b.Property("UseCustomPermissions") .HasColumnType("boolean"); diff --git a/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.Designer.cs b/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.Designer.cs new file mode 100644 index 0000000000..4290c5d93c --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.Designer.cs @@ -0,0 +1,3276 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20251010142244_AddAutoConfirmUserToOrg")] + partial class AddAutoConfirmUserToOrg + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("Filters") + .HasColumnType("TEXT"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ArchivedDate") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.cs b/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.cs new file mode 100644 index 0000000000..7c4d9effbd --- /dev/null +++ b/util/SqliteMigrations/Migrations/20251010142244_AddAutoConfirmUserToOrg.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddAutoConfirmUserToOrg : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UseAutomaticUserConfirmation", + table: "Organization"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index d6b2a90b96..12b97386be 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -215,6 +215,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("UseApi") .HasColumnType("INTEGER"); + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + b.Property("UseCustomPermissions") .HasColumnType("INTEGER"); From 43779cf2e8f61a7afc704df2c4c6b47f78c80ba1 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Mon, 20 Oct 2025 10:17:38 -0400 Subject: [PATCH 32/52] refactor(billing): Remove unused request model (#6463) --- .../OrganizationVerifyBankRequestModel.cs | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/Api/Models/Request/Organizations/OrganizationVerifyBankRequestModel.cs diff --git a/src/Api/Models/Request/Organizations/OrganizationVerifyBankRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationVerifyBankRequestModel.cs deleted file mode 100644 index 71f6873800..0000000000 --- a/src/Api/Models/Request/Organizations/OrganizationVerifyBankRequestModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Bit.Api.Models.Request.Organizations; - -public class OrganizationVerifyBankRequestModel -{ - [Required] - [Range(1, 99)] - public int? Amount1 { get; set; } - [Required] - [Range(1, 99)] - public int? Amount2 { get; set; } -} From c6f1acede9a64c434a662dc0e54204859bada390 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:34:31 -0400 Subject: [PATCH 33/52] [BEEEP] Fix all CA2254 occurrences (#6357) * Fix all CA2254 occurrences * Fix tests --- .editorconfig | 9 +++++++++ bitwarden_license/src/Sso/Startup.cs | 2 +- .../test/Scim.Test/Groups/PatchGroupCommandTests.cs | 2 +- src/Admin/Controllers/HomeController.cs | 4 ++-- src/Admin/Jobs/AliveJob.cs | 2 +- src/Api/Startup.cs | 2 +- src/Api/Tools/Controllers/SendsController.cs | 2 +- src/Api/Utilities/ExceptionHandlerFilterAttribute.cs | 2 +- src/Api/Vault/Controllers/CiphersController.cs | 2 +- .../UpdateOrganizationAuthRequestCommand.cs | 4 ++-- src/Core/Jobs/BaseJobsHostedService.cs | 6 +++--- .../SelfHosted/SelfHostedSyncSponsorshipsCommand.cs | 2 +- .../LoggingExceptionHandlerFilterAttribute.cs | 2 +- .../RequestValidators/BaseRequestValidator.cs | 5 ++--- src/Identity/Startup.cs | 2 +- .../Utilities/ExceptionHandlerFilterAttribute.cs | 2 +- .../Auth/Services/AuthRequestServiceTests.cs | 3 +-- test/Identity.Test/Identity.Test.csproj | 1 + .../IdentityServer/BaseRequestValidatorTests.cs | 10 ++++++---- util/Migrator/DbMigrator.cs | 4 ++-- util/Migrator/DbUpLogger.cs | 12 ++++++------ 21 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.editorconfig b/.editorconfig index 21d7ac4a3a..fd68808456 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 3aeb9c6beb..3ae8883ac4 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -157,6 +157,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); } } diff --git a/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs b/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs index 1b02e62970..8816885ea7 100644 --- a/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Groups/PatchGroupCommandTests.cs @@ -436,7 +436,7 @@ public class PatchGroupCommandTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); // Assert: logging - sutProvider.GetDependency>().ReceivedWithAnyArgs().LogWarning(default); + sutProvider.GetDependency>().ReceivedWithAnyArgs().LogWarning(""); } [Theory] diff --git a/src/Admin/Controllers/HomeController.cs b/src/Admin/Controllers/HomeController.cs index debe5979f5..5b36032ec9 100644 --- a/src/Admin/Controllers/HomeController.cs +++ b/src/Admin/Controllers/HomeController.cs @@ -61,7 +61,7 @@ public class HomeController : Controller } catch (HttpRequestException e) { - _logger.LogError(e, $"Error encountered while sending GET request to {requestUri}"); + _logger.LogError(e, "Error encountered while sending GET request to {RequestUri}", requestUri); return new JsonResult("Unable to fetch latest version") { StatusCode = StatusCodes.Status500InternalServerError }; } @@ -83,7 +83,7 @@ public class HomeController : Controller } catch (HttpRequestException e) { - _logger.LogError(e, $"Error encountered while sending GET request to {requestUri}"); + _logger.LogError(e, "Error encountered while sending GET request to {RequestUri}", requestUri); return new JsonResult("Unable to fetch installed version") { StatusCode = StatusCodes.Status500InternalServerError }; } diff --git a/src/Admin/Jobs/AliveJob.cs b/src/Admin/Jobs/AliveJob.cs index b97d597e58..d62f4cc2cc 100644 --- a/src/Admin/Jobs/AliveJob.cs +++ b/src/Admin/Jobs/AliveJob.cs @@ -22,7 +22,7 @@ public class AliveJob : BaseJob { _logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: Keep alive"); var response = await _httpClient.GetAsync(_globalSettings.BaseServiceUri.Admin); - _logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: Keep alive, " + + _logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: Keep alive, {StatusCode}", response.StatusCode); } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 5d9918d1d4..1519bb25c8 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -326,6 +326,6 @@ public class Startup } // Log startup - logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started."); + logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName); } } diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index c02e9b0c20..c54a9b90c9 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -166,7 +166,7 @@ public class SendsController : Controller } catch (Exception e) { - _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); + _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent)); return; } } diff --git a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs index 91079d5040..1caa7cf841 100644 --- a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs +++ b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs @@ -152,7 +152,7 @@ public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute else { var logger = context.HttpContext.RequestServices.GetRequiredService>(); - logger.LogError(0, exception, exception.Message); + logger.LogError(0, exception, "Unhandled exception"); errorMessage = "An unhandled server error has occurred."; context.HttpContext.Response.StatusCode = 500; } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index eb04ac1210..fe3069d8c7 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1593,7 +1593,7 @@ public class CiphersController : Controller } catch (Exception e) { - _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); + _logger.LogError(e, "Uncaught exception occurred while handling event grid event: {Event}", JsonSerializer.Serialize(eventGridEvent)); return; } } diff --git a/src/Core/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommand.cs b/src/Core/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommand.cs index af966a6e16..9c699a61cb 100644 --- a/src/Core/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommand.cs +++ b/src/Core/AdminConsole/OrganizationAuth/UpdateOrganizationAuthRequestCommand.cs @@ -89,7 +89,7 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque AuthRequestExpiresAfter = _globalSettings.PasswordlessAuth.AdminRequestExpiration } ); - processor.Process((Exception e) => _logger.LogError(e.Message)); + processor.Process((Exception e) => _logger.LogError("Error processing organization auth request: {Message}", e.Message)); await processor.Save((IEnumerable authRequests) => _authRequestRepository.UpdateManyAsync(authRequests)); await processor.SendPushNotifications((ar) => _pushNotificationService.PushAuthRequestResponseAsync(ar)); await processor.SendApprovalEmailsForProcessedRequests(SendApprovalEmail); @@ -114,7 +114,7 @@ public class UpdateOrganizationAuthRequestCommand : IUpdateOrganizationAuthReque // This should be impossible if (user == null) { - _logger.LogError($"User {authRequest.UserId} not found. Trusted device admin approval email not sent."); + _logger.LogError("User {UserId} not found. Trusted device admin approval email not sent.", authRequest.UserId); return; } diff --git a/src/Core/Jobs/BaseJobsHostedService.cs b/src/Core/Jobs/BaseJobsHostedService.cs index 3e7bce7e0f..8b74052f8f 100644 --- a/src/Core/Jobs/BaseJobsHostedService.cs +++ b/src/Core/Jobs/BaseJobsHostedService.cs @@ -107,7 +107,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable throw new Exception("Job failed to start after 10 retries."); } - _logger.LogWarning($"Exception while trying to schedule job: {job.FullName}, {e}"); + _logger.LogWarning(e, "Exception while trying to schedule job: {JobName}", job.FullName); var random = new Random(); await Task.Delay(random.Next(50, 250)); } @@ -125,7 +125,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable continue; } - _logger.LogInformation($"Deleting old job with key {key}"); + _logger.LogInformation("Deleting old job with key {Key}", key); await _scheduler.DeleteJob(key); } @@ -138,7 +138,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable continue; } - _logger.LogInformation($"Unscheduling old trigger with key {key}"); + _logger.LogInformation("Unscheduling old trigger with key {Key}", key); await _scheduler.UnscheduleJob(key); } } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs index 9a995a9cf0..965e0cf2a9 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs @@ -62,7 +62,7 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel .ToDictionary(i => i.SponsoringOrganizationUserId); if (!organizationSponsorshipsDict.Any()) { - _logger.LogInformation($"No existing sponsorships to sync for organization {organizationId}"); + _logger.LogInformation("No existing sponsorships to sync for organization {organizationId}", organizationId); return; } var syncedSponsorships = new List(); diff --git a/src/Core/Utilities/LoggingExceptionHandlerFilterAttribute.cs b/src/Core/Utilities/LoggingExceptionHandlerFilterAttribute.cs index 6709bbb271..300c30641e 100644 --- a/src/Core/Utilities/LoggingExceptionHandlerFilterAttribute.cs +++ b/src/Core/Utilities/LoggingExceptionHandlerFilterAttribute.cs @@ -17,6 +17,6 @@ public class LoggingExceptionHandlerFilterAttribute : ExceptionFilterAttribute var logger = context.HttpContext.RequestServices .GetRequiredService>(); - logger.LogError(0, exception, exception.Message); + logger.LogError(0, exception, "Unhandled exception"); } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 28d4630993..b976775aca 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -268,8 +268,7 @@ public abstract class BaseRequestValidator where T : class if (_globalSettings.SelfHosted) { _logger.LogWarning(Constants.BypassFiltersEventId, - string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".", - $" {CurrentContext.IpAddress}")); + "Failed login attempt. Is2FARequest: {Is2FARequest} IpAddress: {IpAddress}", twoFactorRequest, CurrentContext.IpAddress); } await Task.Delay(2000); // Delay for brute force. @@ -299,7 +298,7 @@ public abstract class BaseRequestValidator where T : class formattedMessage = "Failed login attempt."; break; } - _logger.LogWarning(Constants.BypassFiltersEventId, formattedMessage); + _logger.LogWarning(Constants.BypassFiltersEventId, "{FailedLoginMessage}", formattedMessage); } await Task.Delay(2000); // Delay for brute force. } diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 8da31d87d6..74344977a0 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -240,6 +240,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); } } diff --git a/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs b/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs index 332aa6838c..aba1a6a8dc 100644 --- a/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs +++ b/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs @@ -75,7 +75,7 @@ public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute else { var logger = context.HttpContext.RequestServices.GetRequiredService>(); - logger.LogError(0, exception, exception.Message); + logger.LogError(0, exception, "Unhandled exception"); errorMessage = "An unhandled server error has occurred."; context.HttpContext.Response.StatusCode = 500; } diff --git a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs index 5da0e78422..9c95930c18 100644 --- a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs +++ b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs @@ -467,10 +467,9 @@ public class AuthRequestServiceTests Arg.Any(), Arg.Any()); - var expectedLogMessage = "There are no admin emails to send to."; sutProvider.GetDependency>() .Received(1) - .LogWarning(expectedLogMessage); + .LogWarning("There are no admin emails to send to."); } /// diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index fc0cf07b63..496d652b30 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -5,6 +5,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 6e7c327621..53615cd1d1 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -24,6 +24,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using NSubstitute; using Xunit; @@ -42,7 +43,7 @@ public class BaseRequestValidatorTests private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly ILogger _logger; + private readonly FakeLogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; @@ -65,7 +66,7 @@ public class BaseRequestValidatorTests _deviceValidator = Substitute.For(); _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _logger = Substitute.For>(); + _logger = new FakeLogger(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); _userRepository = Substitute.For(); @@ -120,7 +121,8 @@ public class BaseRequestValidatorTests await _sut.ValidateAsync(context); // Assert - _logger.Received(1).LogWarning(Constants.BypassFiltersEventId, "Failed login attempt. "); + var logs = _logger.Collector.GetSnapshot(true); + Assert.Contains(logs, l => l.Level == LogLevel.Warning && l.Message == "Failed login attempt. Is2FARequest: False IpAddress: "); var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } @@ -356,7 +358,7 @@ public class BaseRequestValidatorTests // 1 -> initial validation passes _sut.isValid = true; - // 2 -> enable the FailedTwoFactorEmail feature flag + // 2 -> enable the FailedTwoFactorEmail feature flag _featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true); // 3 -> set up 2FA as required diff --git a/util/Migrator/DbMigrator.cs b/util/Migrator/DbMigrator.cs index e5e7a569b2..9345a72fc2 100644 --- a/util/Migrator/DbMigrator.cs +++ b/util/Migrator/DbMigrator.cs @@ -53,7 +53,7 @@ public class DbMigrator if (ex.Message.Contains("Server is in script upgrade mode.")) { attempt++; - _logger.LogInformation($"Database is in script upgrade mode, trying again (attempt #{attempt})."); + _logger.LogInformation("Database is in script upgrade mode, trying again (attempt #{Attempt}).", attempt); Thread.Sleep(20000); } else @@ -165,7 +165,7 @@ public class DbMigrator { stringBuilder.AppendLine(script.Name); } - _logger.LogInformation(Constants.BypassFiltersEventId, stringBuilder.ToString()); + _logger.LogInformation(Constants.BypassFiltersEventId, "{Scripts}", stringBuilder.ToString()); return true; } diff --git a/util/Migrator/DbUpLogger.cs b/util/Migrator/DbUpLogger.cs index 2587ce4d80..3f94990ba9 100644 --- a/util/Migrator/DbUpLogger.cs +++ b/util/Migrator/DbUpLogger.cs @@ -15,31 +15,31 @@ public class DbUpLogger : IUpgradeLog public void LogTrace(string format, params object[] args) { - _logger.LogTrace(Constants.BypassFiltersEventId, format, args); + _logger.LogTrace(Constants.BypassFiltersEventId, "{TraceMessage}", string.Format(format, args)); } public void LogDebug(string format, params object[] args) { - _logger.LogDebug(Constants.BypassFiltersEventId, format, args); + _logger.LogDebug(Constants.BypassFiltersEventId, "{DebugMessage}", string.Format(format, args)); } public void LogInformation(string format, params object[] args) { - _logger.LogInformation(Constants.BypassFiltersEventId, format, args); + _logger.LogInformation(Constants.BypassFiltersEventId, "{InfoMessage}", string.Format(format, args)); } public void LogWarning(string format, params object[] args) { - _logger.LogWarning(Constants.BypassFiltersEventId, format, args); + _logger.LogWarning(Constants.BypassFiltersEventId, "{WarningMessage}", string.Format(format, args)); } public void LogError(string format, params object[] args) { - _logger.LogError(Constants.BypassFiltersEventId, format, args); + _logger.LogError(Constants.BypassFiltersEventId, "{ErrorMessage}", string.Format(format, args)); } public void LogError(Exception ex, string format, params object[] args) { - _logger.LogError(Constants.BypassFiltersEventId, ex, format, args); + _logger.LogError(Constants.BypassFiltersEventId, ex, "{ErrorMessage}", string.Format(format, args)); } } From 629672c4b0160ce251614c8061bf9801e4c8d9cc Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Mon, 20 Oct 2025 11:45:11 -0400 Subject: [PATCH 34/52] 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. --- .../AutoFixture/ProfileServiceFixtures.cs | 58 ++ .../IdentityServer/ProfileServiceTests.cs | 558 ++++++++++++++++++ 2 files changed, 616 insertions(+) create mode 100644 test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs create mode 100644 test/Identity.Test/IdentityServer/ProfileServiceTests.cs diff --git a/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs new file mode 100644 index 0000000000..3e6b7e931b --- /dev/null +++ b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Security.Claims; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.Auth.Identity; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.Test.AutoFixture; + +internal class ProfileDataRequestContextCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", Guid.NewGuid().ToString()), + new Claim("name", "Test User"), + new Claim("email", "test@example.com") + ]))) + .With(o => o.Client, new Client { ClientId = "web" }) + .With(o => o.ValidatedRequest, () => null) + .With(o => o.RequestedResources, new ResourceValidationResult()) + .With(o => o.IssuedClaims, []) + .Without(o => o.Caller)); + } +} + +public class ProfileDataRequestContextAttribute : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new ProfileDataRequestContextCustomization(); + } +} + +internal class IsActiveContextCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", Guid.NewGuid().ToString()), + new Claim(Claims.SecurityStamp, "test-security-stamp") + ]))) + .With(o => o.Client, new Client { ClientId = "web" }) + .With(o => o.IsActive, false) + .Without(o => o.Caller)); + } +} + +public class IsActiveContextAttribute : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + { + return new IsActiveContextCustomization(); + } +} diff --git a/test/Identity.Test/IdentityServer/ProfileServiceTests.cs b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs new file mode 100644 index 0000000000..c20a240370 --- /dev/null +++ b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs @@ -0,0 +1,558 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Identity.IdentityServer; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Models; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class ProfileServiceTests +{ + private readonly IUserService _userService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IProviderUserRepository _providerUserRepository; + private readonly IProviderOrganizationRepository _providerOrganizationRepository; + private readonly ILicensingService _licensingService; + private readonly ICurrentContext _currentContext; + private readonly ProfileService _sut; + + public ProfileServiceTests() + { + _userService = Substitute.For(); + _organizationUserRepository = Substitute.For(); + _providerUserRepository = Substitute.For(); + _providerOrganizationRepository = Substitute.For(); + _licensingService = Substitute.For(); + _currentContext = Substitute.For(); + + _sut = new ProfileService( + _userService, + _organizationUserRepository, + _providerUserRepository, + _providerOrganizationRepository, + _licensingService, + _currentContext); + } + + /// + /// For Bitwarden Sends, the zero-knowledge feature architecture is enforced by preserving claims as issued, + /// without attempting user lookup or claims mutation. + /// When acting on behalf of a Send client, the service preserves existing claims, including those issued + /// by the SendAccessGrantValidator, and returns without further claims lookup. + /// + /// + [Theory, BitAutoData] + public async Task GetProfileDataAsync_SendClient_PreservesExistingClaims( + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context) + { + context.Client.ClientId = BitwardenClient.Send; + var existingClaims = new[] + { + new Claim(Claims.SendAccessClaims.SendId, Guid.NewGuid().ToString()), new Claim("send_access", "test") + }; + context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims)); + + await _sut.GetProfileDataAsync(context); + + Assert.Equal(existingClaims.Length, context.IssuedClaims.Count); + Assert.All(existingClaims, existingClaim => + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == existingClaim.Type + && issuedClaim.Value == existingClaim.Value)); + } + + /// + /// For Bitwarden Sends, Send access tokens neither represent a user state nor require user profile data. + /// The SendAccessGrantValidator handles validity of requests, including resource passwords and 2FA. + /// Separation of concerns dictates that actions on behalf of Send clients should complete without + /// further lookup of user data. + /// + /// + [Theory, BitAutoData] + public async Task GetProfileDataAsync_SendClient_DoesNotCallUserService( + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context) + { + context.Client.ClientId = BitwardenClient.Send; + + await _sut.GetProfileDataAsync(context); + + await _userService.DidNotReceive().GetUserByPrincipalAsync(Arg.Any()); + } + + /// + /// For Bitwarden Sends, the client is treated as having always-active behavior, and is neither representative of + /// a user state nor requires user profile data. + /// + /// + [Theory, BitAutoData] + public async Task IsActiveAsync_SendClient_ReturnsTrue( + [AuthFixtures.IsActiveContext] IsActiveContext context) + { + context.Client.ClientId = BitwardenClient.Send; + context.IsActive = false; + + await _sut.IsActiveAsync(context); + + Assert.True(context.IsActive); + } + + /// + /// For Bitwarden Sends, the client should not interrogate the user principal as part of evaluating + /// whether it is active. + /// + [Theory, BitAutoData] + public async Task IsActiveAsync_SendClient_DoesNotCallUserService( + [AuthFixtures.IsActiveContext] IsActiveContext context) + { + context.Client.ClientId = BitwardenClient.Send; + + await _sut.IsActiveAsync(context); + + await _userService.DidNotReceive().GetUserByPrincipalAsync(Arg.Any()); + } + + /// + /// When IdentityServer issues a new access token or services a UserInfo request for a given user, + /// re-evaluate the claims for that user to ensure freshness. + /// Organization-specific claims should be filtered out if the user is null for any reason. + /// This allows users to continue acting on their own behalf from a valid authenticated state, but enforces + /// a security boundary which prevents leaking of organization data and ensures organization claims, + /// which are more likely to change than user claims, are accurate and not present if the user cannot be + /// verified. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_UserNull_PreservesExistingNonOrgClaims( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context) + { + context.Client.ClientId = client; + var existingClaims = new[] + { + new Claim("sub", Guid.NewGuid().ToString()), new Claim("email", "test@example.com"), + new Claim(Claims.OrganizationOwner, Guid.NewGuid().ToString()) // This should be filtered out + }; + context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims)); + _userService.GetUserByPrincipalAsync(context.Subject).Returns((User)null); + + await _sut.GetProfileDataAsync(context); + + // Should preserve user claims + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "sub"); + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "email"); + // Should filter out organization-related claims + Assert.DoesNotContain(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("org")); + } + + /// + /// When IdentityServer issues a new access token or services a UserInfo request for a given user, + /// re-evaluate the claims for that user to ensure freshness. + /// New or updated claims, including premium access and organization or provider membership, + /// should be served with the response. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_UserExists_BuildsIdentityClaims( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub")!.Value); + var orgMemberships = new List + { + new() { Id = Guid.NewGuid(), Type = OrganizationUserType.User } + }; + var providerMemberships = new List(); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(true); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(orgMemberships); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(providerMemberships); + + await _sut.GetProfileDataAsync(context); + + Assert.NotEmpty(context.IssuedClaims); + Assert.Contains(context.IssuedClaims, + issuedClaim => issuedClaim.Type == Claims.Premium && + issuedClaim.Value.Equals("true", StringComparison.CurrentCultureIgnoreCase)); + await _licensingService.Received(1).ValidateUserPremiumAsync(user); + await _currentContext.Received(1).OrganizationMembershipAsync(_organizationUserRepository, user.Id); + await _currentContext.Received(1).ProviderMembershipAsync(_providerUserRepository, user.Id); + } + + /// + /// OpenID Connect Core and JWT distinguish between string and boolean types. For spec compliance, + /// boolean types should be served as booleans, not as strings (e.g., true, not "true"). See + /// https://datatracker.ietf.org/doc/html/rfc7159#section-3, and + /// https://datatracker.ietf.org/doc/html/rfc7519#section-2. + /// For proper claims deserialization and type safety, ensure boolean values are treated as + /// ClaimType.Boolean. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_UserExists_BooleanClaimsHaveBooleanType( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(true); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(new List()); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(new List()); + + await _sut.GetProfileDataAsync(context); + + var booleanClaims = context.IssuedClaims.Where(claim => + claim.Value.Equals("true", StringComparison.OrdinalIgnoreCase) || + claim.Value.Equals("false", StringComparison.OrdinalIgnoreCase)); + + Assert.All(booleanClaims, claim => + Assert.Equal(ClaimValueTypes.Boolean, claim.ValueType)); + } + + /// + /// When IdentityServer issues a new access token or services a UserInfo request for a given user, + /// re-evaluate the claims for that user to ensure freshness. + /// Organization-specific claims should never be allowed to persist, and should always be fetched fresh. + /// + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_FiltersOutOrgClaimsFromExisting( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value); + + var existingClaims = new[] + { + new Claim(Claims.OrganizationOwner, Guid.NewGuid().ToString()), + new Claim(Claims.OrganizationAdmin, Guid.NewGuid().ToString()), new Claim("email", "test@example.com"), + new Claim("name", "Test User") + }; + context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims)); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(false); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(new List()); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(new List()); + + await _sut.GetProfileDataAsync(context); + + Assert.DoesNotContain(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("org")); + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "email"); + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type == "name"); + } + + /// + /// When IdentityServer issues a new access token or services a UserInfo request for a given user, + /// re-evaluate the claims for that user to ensure freshness. + /// Existing claims should always be updated, even if their type exists in the incoming collection. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_NewClaimsOverrideExistingNonOrgClaims( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value); + user.Email = "new@example.com"; + + var existingClaims = new[] + { + new Claim("sub", user.Id.ToString()), new Claim("email", "old@example.com"), + new Claim(Claims.Premium, "false") + }; + context.Subject = new ClaimsPrincipal(new ClaimsIdentity(existingClaims)); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(true); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(new List()); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(new List()); + + await _sut.GetProfileDataAsync(context); + + // Should have new premium claim, not old one + Assert.Contains(context.IssuedClaims, + issuedClaim => issuedClaim.Type == Claims.Premium && + issuedClaim.Value.Equals("true", StringComparison.CurrentCultureIgnoreCase)); + Assert.DoesNotContain(context.IssuedClaims, + issuedClaim => issuedClaim.Type == Claims.Premium && + issuedClaim.Value.Equals("false", StringComparison.CurrentCultureIgnoreCase)); + + // Should have new email + Assert.Contains(context.IssuedClaims, + issuedClaim => issuedClaim.Type == "email" && issuedClaim.Value == "new@example.com"); + Assert.DoesNotContain(context.IssuedClaims, + issuedClaim => issuedClaim.Type == "email" && issuedClaim.Value == "old@example.com"); + } + + /// + /// Users may belong to multiple organizations. Claims should be properly scoped to each relevant organization + /// and not cross-pollinate claims across organizations, and should be fetched fresh on each request. + /// + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_WithMultipleOrganizations_IncludesOrgClaims( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value); + + var orgId1 = Guid.NewGuid(); + var orgId2 = Guid.NewGuid(); + var orgMemberships = new List + { + new() { Id = orgId1, Type = OrganizationUserType.Owner }, + new() { Id = orgId2, Type = OrganizationUserType.Admin } + }; + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(false); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(orgMemberships); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(new List()); + + await _sut.GetProfileDataAsync(context); + + Assert.Contains(context.IssuedClaims, + issuedClaim => issuedClaim.Type == Claims.OrganizationOwner && issuedClaim.Value == orgId1.ToString()); + Assert.Contains(context.IssuedClaims, + issuedClaim => issuedClaim.Type == Claims.OrganizationAdmin && issuedClaim.Value == orgId2.ToString()); + } + + /// + /// Users may belong to providers. Claims should be properly scoped to each relevant provider + /// and not cross-pollinate claims across providers, and should be fetched fresh on each request. + /// + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task GetProfileDataAsync_WithProviders_IncludesProviderClaims( + string client, + [AuthFixtures.ProfileDataRequestContext] + ProfileDataRequestContext context, + User user) + { + context.Client.ClientId = client; + user.Id = Guid.Parse(context.Subject.FindFirst("sub").Value); + + var providerId = Guid.NewGuid(); + var providerMemberships = new List + { + new() { Id = providerId, Type = ProviderUserType.ProviderAdmin } + }; + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + _licensingService.ValidateUserPremiumAsync(user).Returns(false); + _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id) + .Returns(new List()); + _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id) + .Returns(providerMemberships); + + await _sut.GetProfileDataAsync(context); + + Assert.Contains(context.IssuedClaims, issuedClaim => issuedClaim.Type.StartsWith("provider")); + } + + /// + /// Evaluates the happy path for the core session invalidation mechanism. + /// Critical events (e.g., password change) update the security stamp, and any subsequent request through + /// this service should expose the stamp as invalid. A found user and matching security stamp + /// prove out an active session. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task IsActiveAsync_SecurityStampMatches_ReturnsTrue( + string client, + [AuthFixtures.IsActiveContext] IsActiveContext context, + User user) + { + context.Client.ClientId = client; + var securityStamp = "matching-security-stamp"; + user.SecurityStamp = securityStamp; + + context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, securityStamp) + ])); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + + await _sut.IsActiveAsync(context); + + Assert.True(context.IsActive); + } + + /// + /// Critical events (e.g., password change) update the security stamp, and any subsequent request through + /// this service should expose the stamp as invalid. + /// See also examples for stamp invalidation (non-exhaustive): + /// + /// + /// + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task IsActiveAsync_SecurityStampDoesNotMatch_ReturnsFalse( + string client, + [AuthFixtures.IsActiveContext] IsActiveContext context, + User user) + { + context.Client.ClientId = client; + user.SecurityStamp = "current-security-stamp"; + + context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, "old-security-stamp") + ])); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + + await _sut.IsActiveAsync(context); + + Assert.False(context.IsActive); + } + + /// + /// Because security stamps are GUIDs, and database collations, etc., might treat case differently, + /// a case-insensitive comparison is sufficient for proving the match of a security stamp. + /// + [Theory] + [BitAutoData(BitwardenClient.Web, "CuRrEnT-StAmP")] + [BitAutoData(BitwardenClient.Browser, "CuRrEnT-StAmP")] + [BitAutoData(BitwardenClient.Cli, "CuRrEnT-StAmP")] + [BitAutoData(BitwardenClient.Desktop, "CuRrEnT-StAmP")] + [BitAutoData(BitwardenClient.Mobile, "CuRrEnT-StAmP")] + [BitAutoData(BitwardenClient.DirectoryConnector, "CuRrEnT-StAmP")] + public async Task IsActiveAsync_SecurityStampComparison_IsCaseInsensitive( + string client, + string claimStamp, + [AuthFixtures.IsActiveContext] IsActiveContext context, + User user) + { + context.Client.ClientId = client; + user.SecurityStamp = "current-stamp"; + + context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, claimStamp) + ])); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + + await _sut.IsActiveAsync(context); + + Assert.True(context.IsActive); + } + + /// + /// Security stamps should be evaluated when present, but should not always be expected to be present. + /// Given a successful user lookup, absent a security stamp, the session is treated as active. + /// Only if the stamp is presented on context claims should it be validated. + /// + [Theory] + [BitAutoData(BitwardenClient.Web)] + [BitAutoData(BitwardenClient.Browser)] + [BitAutoData(BitwardenClient.Cli)] + [BitAutoData(BitwardenClient.Desktop)] + [BitAutoData(BitwardenClient.Mobile)] + [BitAutoData(BitwardenClient.DirectoryConnector)] + public async Task IsActiveAsync_UserExistsButNoSecurityStampClaim_ReturnsTrue( + string client, + [AuthFixtures.IsActiveContext] IsActiveContext context, + User user) + { + context.Client.ClientId = client; + context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim("sub", user.Id.ToString()), new Claim("email", user.Email) + ])); + + _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); + + await _sut.IsActiveAsync(context); + + Assert.True(context.IsActive); + } +} From 8d52ae869cc1fca9d1a122bbc6d8ac06788cd8b8 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:52:38 +0200 Subject: [PATCH 35/52] Add `pm-26056-consolidated-session-timeout-component` feature flag (#6470) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3bdd884377..54e8b07400 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -201,6 +201,7 @@ public static class FeatureFlagKeys public const string LinuxBiometricsV2 = "pm-26340-linux-biometrics-v2"; public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change"; public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption"; + public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component"; /* Mobile Team */ public const string AndroidImportLoginsFlow = "import-logins-flow"; From 6324f692b8f94c9d9046d23c1527e5ab647ea481 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:03:25 +0200 Subject: [PATCH 36/52] [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 --- .../Enums/PushNotificationLogOutReason.cs | 6 + .../Kdf/Implementations/ChangeKdfCommand.cs | 32 +- .../Data/MasterPasswordAuthenticationData.cs | 3 +- .../Models/Data/MasterPasswordUnlockData.cs | 7 +- src/Core/Models/PushNotification.cs | 6 + .../Platform/Push/IPushNotificationService.cs | 11 +- src/Core/Platform/Push/PushType.cs | 2 +- src/Notifications/HubHelpers.cs | 2 +- .../Controllers/AccountsControllerTest.cs | 418 +++++++++++++++++- .../Controllers/AccountsControllerTests.cs | 51 ++- .../Kdf/ChangeKdfCommandTests.cs | 164 ++++--- .../Push/Engines/AzureQueuePushEngineTests.cs | 26 +- .../NotificationsApiPushEngineTests.cs | 6 +- .../Platform/Push/Engines/PushTestBase.cs | 16 +- .../Push/Engines/RelayPushEngineTests.cs | 10 +- .../NotificationHubPushEngineTests.cs | 12 +- .../AutoFixture/ProfileServiceFixtures.cs | 6 +- .../IdentityServer/ProfileServiceTests.cs | 12 +- 18 files changed, 675 insertions(+), 115 deletions(-) create mode 100644 src/Core/Enums/PushNotificationLogOutReason.cs diff --git a/src/Core/Enums/PushNotificationLogOutReason.cs b/src/Core/Enums/PushNotificationLogOutReason.cs new file mode 100644 index 0000000000..a24f790305 --- /dev/null +++ b/src/Core/Enums/PushNotificationLogOutReason.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Enums; + +public enum PushNotificationLogOutReason : byte +{ + KdfChange = 0 +} diff --git a/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs index fe736f9ac6..83e47c4931 100644 --- a/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs +++ b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Platform.Push; @@ -18,17 +19,22 @@ public class ChangeKdfCommand : IChangeKdfCommand private readonly IUserRepository _userRepository; private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly ILogger _logger; + private readonly IFeatureService _featureService; - public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, IUserRepository userRepository, IdentityErrorDescriber describer, ILogger logger) + public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, + IUserRepository userRepository, IdentityErrorDescriber describer, ILogger logger, + IFeatureService featureService) { _userService = userService; _pushService = pushService; _userRepository = userRepository; _identityErrorDescriber = describer; _logger = logger; + _featureService = featureService; } - public async Task ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData) + public async Task ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, + MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData) { ArgumentNullException.ThrowIfNull(user); if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash)) @@ -37,8 +43,8 @@ public class ChangeKdfCommand : IChangeKdfCommand } // Validate to prevent user account from becoming un-decryptable from invalid parameters - // - // Prevent a de-synced salt value from creating an un-decryptable unlock method + // + // Prevent a de-synced salt value from creating an un-decryptable unlock method authenticationData.ValidateSaltUnchangedForUser(user); unlockData.ValidateSaltUnchangedForUser(user); @@ -47,12 +53,15 @@ public class ChangeKdfCommand : IChangeKdfCommand { throw new BadRequestException("KDF settings must be equal for authentication and unlock."); } + var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf); if (validationErrors.Any()) { throw new BadRequestException("KDF settings are invalid."); } + var logoutOnKdfChange = !_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); + // Update the user with the new KDF settings // This updates the authentication data and unlock data for the user separately. Currently these still // use shared values for KDF settings and salt. @@ -68,7 +77,8 @@ public class ChangeKdfCommand : IChangeKdfCommand // This entire operation MUST be atomic to prevent a user from being locked out of their account. // Salt is ensured to be the same as unlock data, and the value stored in the account and not updated. // KDF is ensured to be the same as unlock data above and updated below. - var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash); + var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash, + refreshStamp: logoutOnKdfChange); if (!result.Succeeded) { _logger.LogWarning("Change KDF failed for user {userId}.", user.Id); @@ -88,7 +98,17 @@ public class ChangeKdfCommand : IChangeKdfCommand user.LastKdfChangeDate = now; await _userRepository.ReplaceAsync(user); - await _pushService.PushLogOutAsync(user.Id); + if (logoutOnKdfChange) + { + await _pushService.PushLogOutAsync(user.Id); + } + else + { + // Clients that support the new feature flag will ignore the logout when it matches the reason and the feature flag is enabled. + await _pushService.PushLogOutAsync(user.Id, reason: PushNotificationLogOutReason.KdfChange); + await _pushService.PushSyncSettingsAsync(user.Id); + } + return IdentityResult.Success; } } diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs index c0ae949a3f..1bc7006cef 100644 --- a/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.KeyManagement.Models.Data; @@ -12,7 +13,7 @@ public class MasterPasswordAuthenticationData { if (user.GetMasterPasswordSalt() != Salt) { - throw new ArgumentException("Invalid master password salt."); + throw new BadRequestException("Invalid master password salt."); } } } diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs index d1ab6f645b..cb18ed2a78 100644 --- a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs @@ -1,6 +1,5 @@ -#nullable enable - -using Bit.Core.Entities; +using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.KeyManagement.Models.Data; @@ -14,7 +13,7 @@ public class MasterPasswordUnlockData { if (user.GetMasterPasswordSalt() != Salt) { - throw new ArgumentException("Invalid master password salt."); + throw new BadRequestException("Invalid master password salt."); } } } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index c4ae1e2858..a622b98e05 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -97,3 +97,9 @@ public class ProviderBankAccountVerifiedPushNotification public Guid ProviderId { get; set; } public Guid AdminId { get; set; } } + +public class LogOutPushNotification +{ + public Guid UserId { get; set; } + public PushNotificationLogOutReason? Reason { get; set; } +} diff --git a/src/Core/Platform/Push/IPushNotificationService.cs b/src/Core/Platform/Push/IPushNotificationService.cs index 32a488b827..b6d7d4d416 100644 --- a/src/Core/Platform/Push/IPushNotificationService.cs +++ b/src/Core/Platform/Push/IPushNotificationService.cs @@ -167,18 +167,17 @@ public interface IPushNotificationService ExcludeCurrentContext = false, }); - Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false) - => PushAsync(new PushNotification + Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false, + PushNotificationLogOutReason? reason = null) + => PushAsync(new PushNotification { Type = PushType.LogOut, Target = NotificationTarget.User, TargetId = userId, - Payload = new UserPushNotification + Payload = new LogOutPushNotification { UserId = userId, -#pragma warning disable BWP0001 // Type or member is obsolete - Date = TimeProvider.GetUtcNow().UtcDateTime, -#pragma warning restore BWP0001 // Type or member is obsolete + Reason = reason }, ExcludeCurrentContext = excludeCurrentContextFromPush, }); diff --git a/src/Core/Platform/Push/PushType.cs b/src/Core/Platform/Push/PushType.cs index 7765c1aa66..93eca86243 100644 --- a/src/Core/Platform/Push/PushType.cs +++ b/src/Core/Platform/Push/PushType.cs @@ -55,7 +55,7 @@ public enum PushType : byte [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] SyncSettings = 10, - [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] + [NotificationInfo("not-specified", typeof(Models.LogOutPushNotification))] LogOut = 11, [NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))] diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 69d5bdc958..0fea72edc3 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -64,7 +64,7 @@ public static class HubHelpers case PushType.SyncSettings: case PushType.LogOut: var userNotification = - JsonSerializer.Deserialize>( + JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); await hubContext.Clients.User(userNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, userNotification, cancellationToken); diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs index 4e5a6850e7..09ec5b010f 100644 --- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs @@ -1,31 +1,81 @@ -using System.Net.Http.Headers; +using System.Net; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.KeyManagement.Models.Requests; using Bit.Api.Models.Response; +using Bit.Core; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; +using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.Controllers; -public class AccountsControllerTest : IClassFixture +public class AccountsControllerTest : IClassFixture, IAsyncLifetime { - private readonly ApiApplicationFactory _factory; + private static readonly string _masterKeyWrappedUserKey = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; - public AccountsControllerTest(ApiApplicationFactory factory) => _factory = factory; + private static readonly string _masterPasswordHash = "master_password_hash"; + private static readonly string _newMasterPasswordHash = "new_master_password_hash"; + + private static readonly KdfRequestModel _defaultKdfRequest = + new() { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 }; + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private readonly IUserRepository _userRepository; + private readonly IPushNotificationService _pushNotificationService; + private readonly IFeatureService _featureService; + private readonly IPasswordHasher _passwordHasher; + + private string _ownerEmail = null!; + + public AccountsControllerTest(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService(_ => { }); + _factory.SubstituteService(_ => { }); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _factory.GetService(); + _pushNotificationService = _factory.GetService(); + _featureService = _factory.GetService(); + _passwordHasher = _factory.GetService>(); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } [Fact] public async Task GetAccountsProfile_success() { - var tokens = await _factory.LoginWithNewAccount(); - var client = _factory.CreateClient(); + await _loginHelper.LoginAsync(_ownerEmail); using var message = new HttpRequestMessage(HttpMethod.Get, "/accounts/profile"); - message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); - var response = await client.SendAsync(message); + var response = await _client.SendAsync(message); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadFromJsonAsync(); Assert.NotNull(content); - Assert.Equal("integration-test@bitwarden.com", content.Email); + Assert.Equal(_ownerEmail, content.Email); Assert.NotNull(content.Name); Assert.True(content.EmailVerified); Assert.False(content.Premium); @@ -35,4 +85,354 @@ public class AccountsControllerTest : IClassFixture Assert.NotNull(content.PrivateKey); Assert.NotNull(content.SecurityStamp); } + + [Theory] + [BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)] + [BitAutoData(KdfType.Argon2id, 4, 65, 5)] + public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOff_SuccessLogout(KdfType kdf, + int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(userBeforeKdfChange); + + _featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(false); + + await _loginHelper.LoginAsync(_ownerEmail); + + var kdfRequest = new KdfRequestModel + { + KdfType = kdf, + Iterations = kdfIterations, + Memory = kdfMemory, + Parallelism = kdfParallelism, + }; + + var response = await PostKdfWithKdfRequestAsync(kdfRequest); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Validate that the user fields were updated correctly + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(user); + Assert.Equal(kdfRequest.KdfType, user.Kdf); + Assert.Equal(kdfRequest.Iterations, user.KdfIterations); + Assert.Equal(kdfRequest.Memory, user.KdfMemory); + Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism); + Assert.Equal(_masterKeyWrappedUserKey, user.Key); + Assert.NotNull(user.LastKdfChangeDate); + Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.NotEqual(userBeforeKdfChange.SecurityStamp, user.SecurityStamp); + Assert.Equal(PasswordVerificationResult.Success, + _passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash)); + + // Validate push notification + await _pushNotificationService.Received(1).PushLogOutAsync(user.Id); + } + + [Theory] + [BitAutoData(KdfType.PBKDF2_SHA256, 600001, null, null)] + [BitAutoData(KdfType.Argon2id, 4, 65, 5)] + public async Task PostKdf_ValidRequestLogoutOnKdfChangeFeatureFlagOn_SuccessSyncAndLogoutWithReason(KdfType kdf, + int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var userBeforeKdfChange = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(userBeforeKdfChange); + + _featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange).Returns(true); + + await _loginHelper.LoginAsync(_ownerEmail); + + var kdfRequest = new KdfRequestModel + { + KdfType = kdf, + Iterations = kdfIterations, + Memory = kdfMemory, + Parallelism = kdfParallelism, + }; + + var response = await PostKdfWithKdfRequestAsync(kdfRequest); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Validate that the user fields were updated correctly + var user = await _userRepository.GetByEmailAsync(_ownerEmail); + Assert.NotNull(user); + Assert.Equal(kdfRequest.KdfType, user.Kdf); + Assert.Equal(kdfRequest.Iterations, user.KdfIterations); + Assert.Equal(kdfRequest.Memory, user.KdfMemory); + Assert.Equal(kdfRequest.Parallelism, user.KdfParallelism); + Assert.Equal(_masterKeyWrappedUserKey, user.Key); + Assert.NotNull(user.LastKdfChangeDate); + Assert.True(user.LastKdfChangeDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.True(user.RevisionDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.True(user.AccountRevisionDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.Equal(userBeforeKdfChange.SecurityStamp, user.SecurityStamp); + Assert.Equal(PasswordVerificationResult.Success, + _passwordHasher.VerifyHashedPassword(user, user.MasterPassword!, _newMasterPasswordHash)); + + // Validate push notification + await _pushNotificationService.Received(1) + .PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange); + await _pushNotificationService.Received(1).PushSyncSettingsAsync(user.Id); + } + + [Fact] + public async Task PostKdf_Unauthorized_ReturnsUnauthorized() + { + // Don't call LoginAsync to test unauthorized access + + var response = await PostKdfWithKdfRequestAsync(_defaultKdfRequest); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Theory] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task PostKdf_AuthenticationDataOrUnlockDataNull_BadRequest(bool authenticationDataNull, + bool unlockDataNull) + { + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = authenticationDataNull + ? null + : new MasterPasswordAuthenticationDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = unlockDataNull + ? null + : new MasterPasswordUnlockDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + var response = await PostKdfAsync(authenticationData, unlockData); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("AuthenticationData and UnlockData must be provided.", content); + } + + [Fact] + public async Task PostKdf_InvalidMasterPasswordHash_BadRequest() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + var requestModel = new PasswordRequestModel + { + MasterPasswordHash = "wrong-master-password-hash", + NewMasterPasswordHash = _newMasterPasswordHash, + Key = _masterKeyWrappedUserKey, + AuthenticationData = authenticationData, + UnlockData = unlockData + }; + + using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf"); + message.Content = JsonContent.Create(requestModel); + var response = await _client.SendAsync(message); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Incorrect password", content); + } + + [Fact] + public async Task PostKdf_ChangedSaltInAuthenticationData_BadRequest() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = "wrong-salt@bitwarden.com" + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + var response = await PostKdfAsync(authenticationData, unlockData); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Invalid master password salt.", content); + } + + [Fact] + public async Task PostKdf_ChangedSaltInUnlockData_BadRequest() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = "wrong-salt@bitwarden.com" + }; + + var response = await PostKdfAsync(authenticationData, unlockData); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Invalid master password salt.", content); + } + + [Fact] + public async Task PostKdf_KdfNotMatching_BadRequest() + { + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_000 }, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600_001 }, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + var response = await PostKdfAsync(authenticationData, unlockData); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("KDF settings must be equal for authentication and unlock.", content); + } + + [Theory] + [InlineData(KdfType.PBKDF2_SHA256, 1, null, null)] + [InlineData(KdfType.Argon2id, 4, null, 5)] + [InlineData(KdfType.Argon2id, 4, 65, null)] + public async Task PostKdf_InvalidKdf_BadRequest(KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + await _loginHelper.LoginAsync(_ownerEmail); + + var kdfRequest = new KdfRequestModel + { + KdfType = kdf, + Iterations = kdfIterations, + Memory = kdfMemory, + Parallelism = kdfParallelism + }; + + var response = await PostKdfWithKdfRequestAsync(kdfRequest); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("KDF settings are invalid", content); + } + + [Fact] + public async Task PostKdf_InvalidNewMasterPassword_BadRequest() + { + var newMasterPasswordHash = "too-short"; + + await _loginHelper.LoginAsync(_ownerEmail); + + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterPasswordAuthenticationHash = newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = _defaultKdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + var requestModel = new PasswordRequestModel + { + MasterPasswordHash = _masterPasswordHash, + NewMasterPasswordHash = newMasterPasswordHash, + Key = _masterKeyWrappedUserKey, + AuthenticationData = authenticationData, + UnlockData = unlockData + }; + + using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf"); + message.Content = JsonContent.Create(requestModel); + var response = await _client.SendAsync(message); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("Passwords must be at least", content); + } + + private async Task PostKdfWithKdfRequestAsync(KdfRequestModel kdfRequest) + { + var authenticationData = new MasterPasswordAuthenticationDataRequestModel + { + Kdf = kdfRequest, + MasterPasswordAuthenticationHash = _newMasterPasswordHash, + Salt = _ownerEmail + }; + + var unlockData = new MasterPasswordUnlockDataRequestModel + { + Kdf = kdfRequest, + MasterKeyWrappedUserKey = _masterKeyWrappedUserKey, + Salt = _ownerEmail + }; + + return await PostKdfAsync(authenticationData, unlockData); + } + + private async Task PostKdfAsync( + MasterPasswordAuthenticationDataRequestModel? authenticationDataRequest, + MasterPasswordUnlockDataRequestModel? unlockDataRequest) + { + var requestModel = new PasswordRequestModel + { + MasterPasswordHash = _masterPasswordHash, + NewMasterPasswordHash = _newMasterPasswordHash, + Key = _masterKeyWrappedUserKey, + AuthenticationData = authenticationDataRequest, + UnlockData = unlockDataRequest + }; + + using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/kdf"); + message.Content = JsonContent.Create(requestModel); + return await _client.SendAsync(message); + } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index e2959a119c..f1aa11d068 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -11,6 +11,7 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -617,6 +618,16 @@ public class AccountsControllerTests : IDisposable await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user); } + [Theory] + [BitAutoData] + public async Task PostKdf_UserNotFound_ShouldFail(PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(null)); + + // Act + await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + } + [Theory] [BitAutoData] public async Task PostKdf_WithNullAuthenticationData_ShouldFail( @@ -626,7 +637,9 @@ public class AccountsControllerTests : IDisposable model.AuthenticationData = null; // Act - await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + var exception = await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + + Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message); } [Theory] @@ -638,7 +651,41 @@ public class AccountsControllerTests : IDisposable model.UnlockData = null; // Act - await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + var exception = await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + + Assert.Contains("AuthenticationData and UnlockData must be provided.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task PostKdf_ChangeKdfFailed_ShouldFail( + User user, PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + _changeKdfCommand.ChangeKdfAsync(Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Failed(new IdentityError { Description = "Change KDF failed" }))); + + // Act + var exception = await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + + Assert.NotNull(exception.ModelState); + Assert.Contains("Change KDF failed", + exception.ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage)); + } + + [Theory] + [BitAutoData] + public async Task PostKdf_ChangeKdfSuccess_NoError( + User user, PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + _changeKdfCommand.ChangeKdfAsync(Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + + // Act + await _sut.PostKdf(model); } // Below are helper functions that currently belong to this diff --git a/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs index 02e04b9ce9..991935b928 100644 --- a/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs +++ b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs @@ -1,9 +1,11 @@ #nullable enable using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Kdf.Implementations; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -21,16 +23,12 @@ public class ChangeKdfCommandTests [BitAutoData] public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -59,13 +57,7 @@ public class ChangeKdfCommandTests [BitAutoData] public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider sutProvider) { - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -85,17 +77,13 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider sutProvider, + User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(false)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(false)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -116,7 +104,9 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider sutProvider, User user) + public async Task + ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut( + SutProvider sutProvider, User user) { var constantKdf = new KdfSettings { @@ -137,8 +127,12 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = user.GetMasterPasswordSalt() }; - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .UpdatePasswordHash(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().IsEnabled(Arg.Any()).Returns(false); await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); @@ -150,17 +144,79 @@ public class ChangeKdfCommandTests && u.KdfParallelism == constantKdf.Parallelism && u.Key == "new-wrapped-key" )); + await sutProvider.GetDependency().Received(1).UpdatePasswordHash(user, + authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true); + await sutProvider.GetDependency().Received(1).PushLogOutAsync(user.Id); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task + ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + var constantKdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 5, + Memory = 1024, + Parallelism = 4 + }; + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = constantKdf, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = constantKdf, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = user.GetMasterPasswordSalt() + }; + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + sutProvider.GetDependency() + .UpdatePasswordHash(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(IdentityResult.Success)); + sutProvider.GetDependency().IsEnabled(Arg.Any()).Returns(true); + + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is(u => + u.Id == user.Id + && u.Kdf == constantKdf.KdfType + && u.KdfIterations == constantKdf.Iterations + && u.KdfMemory == constantKdf.Memory + && u.KdfParallelism == constantKdf.Parallelism + && u.Key == "new-wrapped-key" + )); + await sutProvider.GetDependency().Received(1).UpdatePasswordHash(user, + authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false); + await sutProvider.GetDependency().Received(1) + .PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange); + await sutProvider.GetDependency().Received(1).PushSyncSettingsAsync(user.Id); + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException( + SutProvider sutProvider, User user) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var authenticationData = new MasterPasswordAuthenticationData { - Kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }, + Kdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 4, + Memory = 512, + Parallelism = 4 + }, MasterPasswordAuthenticationHash = "new-auth-hash", Salt = user.GetMasterPasswordSalt() }; @@ -176,9 +232,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider sutProvider, User user, KdfSettings kdf) + public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider sutProvider, User user, + KdfSettings kdf) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var authenticationData = new MasterPasswordAuthenticationData { @@ -192,15 +250,17 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = user.GetMasterPasswordSalt() }; - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider sutProvider, User user, KdfSettings kdf) + public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider sutProvider, User user, + KdfSettings kdf) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var authenticationData = new MasterPasswordAuthenticationData { @@ -214,25 +274,22 @@ public class ChangeKdfCommandTests MasterKeyWrappedUserKey = "new-wrapped-key", Salt = "different-salt" }; - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); } [Theory] [BitAutoData] - public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider sutProvider, + User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" }); - sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()).Returns(Task.FromResult(failedResult)); + sutProvider.GetDependency().UpdatePasswordHash(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(failedResult)); - var kdf = new KdfSettings - { - KdfType = Enums.KdfType.Argon2id, - Iterations = 4, - Memory = 512, - Parallelism = 4 - }; + var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; var authenticationData = new MasterPasswordAuthenticationData { Kdf = kdf, @@ -253,9 +310,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); // Create invalid KDF settings (iterations too low for PBKDF2) var invalidKdf = new KdfSettings @@ -287,9 +346,11 @@ public class ChangeKdfCommandTests [Theory] [BitAutoData] - public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider sutProvider, User user) + public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException( + SutProvider sutProvider, User user) { - sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); // Create invalid Argon2 KDF settings (memory too high) var invalidKdf = new KdfSettings @@ -318,5 +379,4 @@ public class ChangeKdfCommandTests Assert.Equal("KDF settings are invalid.", exception.Message); } - } diff --git a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs index 9c46211517..3f31f1fad4 100644 --- a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs @@ -358,20 +358,28 @@ public class AzureQueuePushEngineTests } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); + var payload = new JsonObject + { + ["UserId"] = userId + }; + if (reason != null) + { + payload["Reason"] = (int)reason; + } + var expectedPayload = new JsonObject { ["Type"] = 11, - ["Payload"] = new JsonObject - { - ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, - }, + ["Payload"] = payload, }; if (excludeCurrentContext) @@ -380,7 +388,7 @@ public class AzureQueuePushEngineTests } await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), expectedPayload ); } diff --git a/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs index c61c2f37d0..7f230c4e5c 100644 --- a/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs @@ -1,6 +1,7 @@ using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push.Internal; using Bit.Core.Tools.Entities; @@ -193,7 +194,8 @@ public class NotificationsApiPushEngineTests : PushTestBase }; } - protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) + protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null; @@ -203,7 +205,7 @@ public class NotificationsApiPushEngineTests : PushTestBase ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + ["Reason"] = reason != null ? (int)reason : null }, ["ContextId"] = contextId, }; diff --git a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs index e0eeeda97d..c0037f57aa 100644 --- a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs @@ -86,7 +86,8 @@ public abstract class PushTestBase protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId); protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId); protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId); - protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext); + protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext, + PushNotificationLogOutReason? reason); protected abstract JsonNode GetPushSendCreatePayload(Send send); protected abstract JsonNode GetPushSendUpdatePayload(Send send); protected abstract JsonNode GetPushSendDeletePayload(Send send); @@ -263,15 +264,18 @@ public abstract class PushTestBase } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), - GetPushLogOutPayload(userId, excludeCurrentContext) + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), + GetPushLogOutPayload(userId, excludeCurrentContext, reason) ); } diff --git a/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs index 010ad40d13..f8ae07f647 100644 --- a/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs @@ -4,6 +4,7 @@ using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; @@ -64,7 +65,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["UserId"] = cipher.UserId, ["OrganizationId"] = null, // Currently CollectionIds are not passed along from the method signature - // to the request body. + // to the request body. ["CollectionIds"] = null, ["RevisionDate"] = cipher.RevisionDate, }, @@ -88,7 +89,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["UserId"] = cipher.UserId, ["OrganizationId"] = null, // Currently CollectionIds are not passed along from the method signature - // to the request body. + // to the request body. ["CollectionIds"] = null, ["RevisionDate"] = cipher.RevisionDate, }, @@ -274,7 +275,8 @@ public class RelayPushNotificationServiceTests : PushTestBase }; } - protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) + protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext, + PushNotificationLogOutReason? reason) { JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null; @@ -288,7 +290,7 @@ public class RelayPushNotificationServiceTests : PushTestBase ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + ["Reason"] = reason != null ? (int)reason : null }, ["ClientType"] = null, ["InstallationId"] = null, diff --git a/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs index a32b112675..f5f257c741 100644 --- a/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs @@ -404,16 +404,18 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext) + [InlineData(true, null)] + [InlineData(true, PushNotificationLogOutReason.KdfChange)] + [InlineData(false, null)] + [InlineData(false, PushNotificationLogOutReason.KdfChange)] + public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext, PushNotificationLogOutReason? reason) { var userId = Guid.NewGuid(); var expectedPayload = new JsonObject { ["UserId"] = userId, - ["Date"] = _now, + ["Reason"] = reason != null ? (int)reason : null, }; var expectedTag = excludeCurrentContext @@ -421,7 +423,7 @@ public class NotificationHubPushNotificationServiceTests : $"(template:payload_userId:{userId})"; await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext, reason), PushType.LogOut, expectedPayload, expectedTag diff --git a/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs index 3e6b7e931b..aaf8b269d6 100644 --- a/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs +++ b/test/Identity.Test/AutoFixture/ProfileServiceFixtures.cs @@ -15,8 +15,8 @@ internal class ProfileDataRequestContextCustomization : ICustomization fixture.Customize(composer => composer .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ new Claim("sub", Guid.NewGuid().ToString()), - new Claim("name", "Test User"), - new Claim("email", "test@example.com") + new Claim("name", "Test User"), + new Claim("email", "test@example.com") ]))) .With(o => o.Client, new Client { ClientId = "web" }) .With(o => o.ValidatedRequest, () => null) @@ -41,7 +41,7 @@ internal class IsActiveContextCustomization : ICustomization fixture.Customize(composer => composer .With(o => o.Subject, new ClaimsPrincipal(new ClaimsIdentity([ new Claim("sub", Guid.NewGuid().ToString()), - new Claim(Claims.SecurityStamp, "test-security-stamp") + new Claim(Claims.SecurityStamp, "test-security-stamp") ]))) .With(o => o.Client, new Client { ClientId = "web" }) .With(o => o.IsActive, false) diff --git a/test/Identity.Test/IdentityServer/ProfileServiceTests.cs b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs index c20a240370..c467f074ac 100644 --- a/test/Identity.Test/IdentityServer/ProfileServiceTests.cs +++ b/test/Identity.Test/IdentityServer/ProfileServiceTests.cs @@ -452,7 +452,8 @@ public class ProfileServiceTests user.SecurityStamp = securityStamp; context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ - new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, securityStamp) + new Claim("sub", user.Id.ToString()), + new Claim(Claims.SecurityStamp, securityStamp) ])); _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); @@ -486,7 +487,8 @@ public class ProfileServiceTests user.SecurityStamp = "current-security-stamp"; context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ - new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, "old-security-stamp") + new Claim("sub", user.Id.ToString()), + new Claim(Claims.SecurityStamp, "old-security-stamp") ])); _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); @@ -517,7 +519,8 @@ public class ProfileServiceTests user.SecurityStamp = "current-stamp"; context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ - new Claim("sub", user.Id.ToString()), new Claim(Claims.SecurityStamp, claimStamp) + new Claim("sub", user.Id.ToString()), + new Claim(Claims.SecurityStamp, claimStamp) ])); _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); @@ -546,7 +549,8 @@ public class ProfileServiceTests { context.Client.ClientId = client; context.Subject = new ClaimsPrincipal(new ClaimsIdentity([ - new Claim("sub", user.Id.ToString()), new Claim("email", user.Email) + new Claim("sub", user.Id.ToString()), + new Claim("email", user.Email) ])); _userService.GetUserByPrincipalAsync(context.Subject).Returns(user); From 9c51c9971b17550a271f2b2db2c78dab962997c7 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:07:55 -0500 Subject: [PATCH 37/52] [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> --- .../RemoveOrganizationFromProviderCommand.cs | 16 +- .../Services/ProviderBillingService.cs | 3 +- ...oveOrganizationFromProviderCommandTests.cs | 23 +- .../Controllers/MigrateProvidersController.cs | 83 ---- .../Models/MigrateProvidersRequestModel.cs | 13 - .../Views/MigrateProviders/Details.cshtml | 39 -- .../Views/MigrateProviders/Index.cshtml | 46 -- .../Views/MigrateProviders/Results.cshtml | 28 -- src/Admin/Controllers/ToolsController.cs | 139 ------ src/Admin/Enums/Permissions.cs | 4 +- src/Admin/Models/StripeSubscriptionsModel.cs | 45 -- src/Admin/Startup.cs | 2 - src/Admin/Utilities/RolePermissionMapping.cs | 8 +- src/Admin/Views/Shared/_Layout.cshtml | 16 +- .../Views/Tools/StripeSubscriptions.cshtml | 277 ----------- .../Controllers/ProviderBillingController.cs | 2 +- .../Responses/ProviderSubscriptionResponse.cs | 9 +- src/Billing/BillingSettings.cs | 4 +- src/Billing/Controllers/StripeController.cs | 4 +- .../Implementations/InvoiceCreatedHandler.cs | 7 +- .../Implementations/PaymentFailedHandler.cs | 22 +- .../PaymentSucceededHandler.cs | 15 +- .../Implementations/ProviderEventService.cs | 17 +- .../StripeEventUtilityService.cs | 34 +- .../SubscriptionDeletedHandler.cs | 5 +- .../SubscriptionUpdatedHandler.cs | 23 +- .../Implementations/UpcomingInvoiceHandler.cs | 27 +- src/Billing/appsettings.json | 6 +- .../Billing/Extensions/InvoiceExtensions.cs | 6 +- .../Extensions/SubscriptionExtensions.cs | 25 + .../UpcomingInvoiceOptionsExtensions.cs | 35 -- src/Core/Billing/Models/BillingHistoryInfo.cs | 3 +- .../Commands/PreviewOrganizationTaxCommand.cs | 20 +- .../Organizations/Models/OrganizationSale.cs | 2 +- .../Queries/GetOrganizationWarningsQuery.cs | 22 +- .../Services/OrganizationBillingService.cs | 16 +- ...tePremiumCloudHostedSubscriptionCommand.cs | 3 +- .../Commands/PreviewPremiumTaxCommand.cs | 2 +- .../Models/ClientMigrationTracker.cs | 26 -- .../Models/ProviderMigrationResult.cs | 48 -- .../Models/ProviderMigrationTracker.cs | 25 - .../Migration/ServiceCollectionExtensions.cs | 15 - .../Services/IMigrationTrackerCache.cs | 17 - .../Services/IOrganizationMigrator.cs | 8 - .../Migration/Services/IProviderMigrator.cs | 10 - .../MigrationTrackerDistributedCache.cs | 110 ----- .../Implementations/OrganizationMigrator.cs | 331 ------------- .../Implementations/ProviderMigrator.cs | 436 ------------------ .../PremiumUserBillingService.cs | 3 +- .../Commands/RestartSubscriptionCommand.cs | 5 +- src/Core/Core.csproj | 2 +- src/Core/Models/Business/SubscriptionInfo.cs | 10 +- .../Stripe/StripeSubscriptionListOptions.cs | 51 -- src/Core/Services/IStripeAdapter.cs | 79 ++-- .../Services/Implementations/StripeAdapter.cs | 140 +++--- .../Implementations/StripePaymentService.cs | 259 ++++++----- src/Sql/Sql.sqlproj | 3 - .../ProviderBillingControllerTests.cs | 121 ++++- test/Billing.Test/Billing.Test.csproj | 21 - .../Resources/Events/charge.succeeded.json | 130 ------ .../Events/customer.subscription.updated.json | 177 ------- .../Resources/Events/customer.updated.json | 311 ------------- .../Resources/Events/invoice.created.json | 222 --------- .../Resources/Events/invoice.finalized.json | 400 ---------------- .../Resources/Events/invoice.upcoming.json | 225 --------- .../Events/payment_method.attached.json | 63 --- .../Services/ProviderEventServiceTests.cs | 93 +++- .../SubscriptionDeletedHandlerTests.cs | 53 ++- .../SubscriptionUpdatedHandlerTests.cs | 118 ++++- .../Utilities/StripeTestEvents.cs | 35 -- .../Extensions/InvoiceExtensionsTests.cs | 9 +- .../Business/OrganizationLicenseTests.cs | 12 +- .../Models/Business/UserLicenseTests.cs | 12 +- .../PreviewOrganizationTaxCommandTests.cs | 94 ++-- .../GetCloudOrganizationLicenseQueryTests.cs | 33 +- .../GetOrganizationWarningsQueryTests.cs | 11 +- ...miumCloudHostedSubscriptionCommandTests.cs | 90 +++- .../Commands/PreviewPremiumTaxCommandTests.cs | 16 +- .../OrganizationBillingServiceTests.cs | 233 ++++++++++ .../RestartSubscriptionCommandTests.cs | 24 +- .../Services/StripePaymentServiceTests.cs | 100 ++-- 81 files changed, 1273 insertions(+), 3959 deletions(-) delete mode 100644 src/Admin/Billing/Controllers/MigrateProvidersController.cs delete mode 100644 src/Admin/Billing/Models/MigrateProvidersRequestModel.cs delete mode 100644 src/Admin/Billing/Views/MigrateProviders/Details.cshtml delete mode 100644 src/Admin/Billing/Views/MigrateProviders/Index.cshtml delete mode 100644 src/Admin/Billing/Views/MigrateProviders/Results.cshtml delete mode 100644 src/Admin/Models/StripeSubscriptionsModel.cs delete mode 100644 src/Admin/Views/Tools/StripeSubscriptions.cshtml create mode 100644 src/Core/Billing/Extensions/SubscriptionExtensions.cs delete mode 100644 src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs delete mode 100644 src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs delete mode 100644 src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs delete mode 100644 src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs delete mode 100644 src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs delete mode 100644 src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs delete mode 100644 src/Core/Models/Stripe/StripeSubscriptionListOptions.cs delete mode 100644 test/Billing.Test/Resources/Events/charge.succeeded.json delete mode 100644 test/Billing.Test/Resources/Events/customer.subscription.updated.json delete mode 100644 test/Billing.Test/Resources/Events/customer.updated.json delete mode 100644 test/Billing.Test/Resources/Events/invoice.created.json delete mode 100644 test/Billing.Test/Resources/Events/invoice.finalized.json delete mode 100644 test/Billing.Test/Resources/Events/invoice.upcoming.json delete mode 100644 test/Billing.Test/Resources/Events/payment_method.attached.json delete mode 100644 test/Billing.Test/Utilities/StripeTestEvents.cs diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 9ade2d660a..994b305349 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -148,22 +148,30 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv } else if (organization.IsStripeEnabled()) { - var subscription = await _stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId); + var subscription = await _stripeAdapter.SubscriptionGetAsync(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.CustomerUpdateAsync(subscription.CustomerId, new CustomerUpdateOptions { - Coupon = string.Empty, Email = organization.BillingEmail }); + if (subscription.Customer.Discount?.Coupon != null) + { + await _stripeAdapter.CustomerDeleteDiscountAsync(subscription.CustomerId); + } + await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, - DaysUntilDue = 30 + DaysUntilDue = 30, }); await _subscriberService.RemovePaymentSource(organization); diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs index c9851eb403..e352297f1e 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -481,7 +481,6 @@ public class ProviderBillingService( City = billingAddress.City, State = billingAddress.State }, - Coupon = !string.IsNullOrEmpty(provider.DiscountId) ? provider.DiscountId : null, Description = provider.DisplayBusinessName(), Email = provider.BillingEmail, InvoiceSettings = new CustomerInvoiceSettingsOptions @@ -663,6 +662,7 @@ public class ProviderBillingService( : 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 { { "providerId", provider.Id.ToString() } }, OffSession = true, @@ -671,7 +671,6 @@ public class ProviderBillingService( AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; - try { var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 9b9c41048b..2bb02c3cee 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -156,16 +156,18 @@ public class RemoveOrganizationFromProviderCommandTests "b@example.com" ]); - sutProvider.GetDependency().SubscriptionGetAsync(organization.GatewaySubscriptionId) - .Returns(GetSubscription(organization.GatewaySubscriptionId)); + sutProvider.GetDependency().SubscriptionGetAsync(organization.GatewaySubscriptionId, Arg.Is( + options => options.Expand.Contains("customer"))) + .Returns(GetSubscription(organization.GatewaySubscriptionId, organization.GatewayCustomerId)); await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); var stripeAdapter = sutProvider.GetDependency(); await stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, - Arg.Is(options => - options.Coupon == string.Empty && options.Email == "a@example.com")); + Arg.Is(options => options.Email == "a@example.com")); + + await stripeAdapter.Received(1).CustomerDeleteDiscountAsync(organization.GatewayCustomerId); await stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, Arg.Is(options => @@ -368,10 +370,21 @@ public class RemoveOrganizationFromProviderCommandTests Arg.Is>(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 { diff --git a/src/Admin/Billing/Controllers/MigrateProvidersController.cs b/src/Admin/Billing/Controllers/MigrateProvidersController.cs deleted file mode 100644 index ef5ea2312e..0000000000 --- a/src/Admin/Billing/Controllers/MigrateProvidersController.cs +++ /dev/null @@ -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 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 ResultsAsync(MigrateProvidersRequestModel request) - { - var providerIds = GetProviderIdsFromInput(request.ProviderIds); - - if (providerIds.Count == 0) - { - return View(Array.Empty()); - } - - var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult)); - - return View(results); - } - - [HttpGet("results/{providerId:guid}")] - [RequirePermission(Permission.Tools_MigrateProviders)] - public async Task DetailsAsync([FromRoute] Guid providerId) - { - var result = await providerMigrator.GetResult(providerId); - - if (result == null) - { - return RedirectToAction("Index"); - } - - return View(result); - } - - private static List GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text) - ? text.Split( - ["\r\n", "\r", "\n"], - StringSplitOptions.TrimEntries - ) - .Select(id => new Guid(id)) - .ToList() - : []; -} diff --git a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs deleted file mode 100644 index 273f934eba..0000000000 --- a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs +++ /dev/null @@ -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; } -} diff --git a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml deleted file mode 100644 index 6ee0344057..0000000000 --- a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml +++ /dev/null @@ -1,39 +0,0 @@ -@using System.Text.Json -@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult -@{ - ViewData["Title"] = "Results"; -} - -

Migrate Providers

-

Migration Details: @Model.ProviderName

-
-
Id
-
@Model.ProviderId
- -
Result
-
@Model.Result
-
-

Client Organizations

-
- - - - - - - - - - - @foreach (var clientResult in Model.Clients) - { - - - - - - - } - -
IDNameResultPrevious State
@clientResult.OrganizationId@clientResult.OrganizationName@clientResult.Result
@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))
-
diff --git a/src/Admin/Billing/Views/MigrateProviders/Index.cshtml b/src/Admin/Billing/Views/MigrateProviders/Index.cshtml deleted file mode 100644 index 0aed94c25d..0000000000 --- a/src/Admin/Billing/Views/MigrateProviders/Index.cshtml +++ /dev/null @@ -1,46 +0,0 @@ -@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel; -@{ - ViewData["Title"] = "Migrate Providers"; -} - -

Migrate Providers

-

Bulk Consolidated Billing Migration Tool

-
-

- This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing. - Because of the expensive nature of the operation, you can only migrate 10 Providers at a time. -

-

- Updates made through this tool are irreversible without manual intervention. -

-

Example Input (Please enter each Provider ID separated by a new line):

-
-
-
f513affc-2290-4336-879e-21ec3ecf3e78
-f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
-bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
-174e82fc-70c3-448d-9fe7-00bad2a3ab00
-22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14
-
-
-
-
-
- - -
-
- -
-
-
-
-
- - -
-
- -
-
-
diff --git a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml deleted file mode 100644 index 94db08db3d..0000000000 --- a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml +++ /dev/null @@ -1,28 +0,0 @@ -@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[] -@{ - ViewData["Title"] = "Results"; -} - -

Migrate Providers

-

Results

-
- - - - - - - - - - @foreach (var result in Model) - { - - - - - - } - -
IDNameResult
@result.ProviderId@result.ProviderName@result.Result
-
diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index b754b1f968..46dafd65e7 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using System.Text; using System.Text.Json; using Bit.Admin.Enums; using Bit.Admin.Models; @@ -10,7 +9,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Entities; -using Bit.Core.Models.BitStripe; using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; @@ -33,7 +31,6 @@ public class ToolsController : Controller private readonly IInstallationRepository _installationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProviderUserRepository _providerUserRepository; - private readonly IPaymentService _paymentService; private readonly IStripeAdapter _stripeAdapter; private readonly IWebHostEnvironment _environment; @@ -46,7 +43,6 @@ public class ToolsController : Controller IInstallationRepository installationRepository, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, - IPaymentService paymentService, IStripeAdapter stripeAdapter, IWebHostEnvironment environment) { @@ -58,7 +54,6 @@ public class ToolsController : Controller _installationRepository = installationRepository; _organizationUserRepository = organizationUserRepository; _providerUserRepository = providerUserRepository; - _paymentService = paymentService; _stripeAdapter = stripeAdapter; _environment = environment; } @@ -341,138 +336,4 @@ public class ToolsController : Controller throw new Exception("No license to generate."); } } - - [RequirePermission(Permission.Tools_ManageStripeSubscriptions)] - public async Task StripeSubscriptions(StripeSubscriptionListOptions options) - { - options = options ?? new StripeSubscriptionListOptions(); - options.Limit = 10; - options.Expand = new List() { "data.customer", "data.latest_invoice" }; - options.SelectAll = false; - - var subscriptions = await _stripeAdapter.SubscriptionListAsync(options); - - options.StartingAfter = subscriptions.LastOrDefault()?.Id; - options.EndingBefore = await StripeSubscriptionsGetHasPreviousPage(subscriptions, options) ? - subscriptions.FirstOrDefault()?.Id : - null; - - var isProduction = _environment.IsProduction(); - var model = new StripeSubscriptionsModel() - { - Items = subscriptions.Select(s => new StripeSubscriptionRowModel(s)).ToList(), - Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data, - TestClocks = isProduction ? new List() : await _stripeAdapter.TestClockListAsync(), - Filter = options - }; - return View(model); - } - - [HttpPost] - [RequirePermission(Permission.Tools_ManageStripeSubscriptions)] - public async Task StripeSubscriptions([FromForm] StripeSubscriptionsModel model) - { - if (!ModelState.IsValid) - { - var isProduction = _environment.IsProduction(); - model.Prices = (await _stripeAdapter.PriceListAsync(new Stripe.PriceListOptions() { Limit = 100 })).Data; - model.TestClocks = isProduction ? new List() : await _stripeAdapter.TestClockListAsync(); - return View(model); - } - - if (model.Action == StripeSubscriptionsAction.Export || model.Action == StripeSubscriptionsAction.BulkCancel) - { - var subscriptions = model.Filter.SelectAll ? - await _stripeAdapter.SubscriptionListAsync(model.Filter) : - model.Items.Where(x => x.Selected).Select(x => x.Subscription); - - if (model.Action == StripeSubscriptionsAction.Export) - { - return StripeSubscriptionsExport(subscriptions); - } - - if (model.Action == StripeSubscriptionsAction.BulkCancel) - { - await StripeSubscriptionsCancel(subscriptions); - } - } - else - { - if (model.Action == StripeSubscriptionsAction.PreviousPage || model.Action == StripeSubscriptionsAction.Search) - { - model.Filter.StartingAfter = null; - } - - if (model.Action == StripeSubscriptionsAction.NextPage || model.Action == StripeSubscriptionsAction.Search) - { - if (!string.IsNullOrEmpty(model.Filter.StartingAfter)) - { - var subscription = await _stripeAdapter.SubscriptionGetAsync(model.Filter.StartingAfter); - if (subscription.Status == "canceled") - { - model.Filter.StartingAfter = null; - } - } - model.Filter.EndingBefore = null; - } - } - - - return RedirectToAction("StripeSubscriptions", model.Filter); - } - - // This requires a redundant API call to Stripe because of the way they handle pagination. - // The StartingBefore value has to be inferred from the list we get, and isn't supplied by Stripe. - private async Task StripeSubscriptionsGetHasPreviousPage(List subscriptions, StripeSubscriptionListOptions options) - { - var hasPreviousPage = false; - if (subscriptions.FirstOrDefault()?.Id != null) - { - var previousPageSearchOptions = new StripeSubscriptionListOptions() - { - EndingBefore = subscriptions.FirstOrDefault().Id, - Limit = 1, - Status = options.Status, - CurrentPeriodEndDate = options.CurrentPeriodEndDate, - CurrentPeriodEndRange = options.CurrentPeriodEndRange, - Price = options.Price - }; - hasPreviousPage = (await _stripeAdapter.SubscriptionListAsync(previousPageSearchOptions)).Count > 0; - } - return hasPreviousPage; - } - - private async Task StripeSubscriptionsCancel(IEnumerable subscriptions) - { - foreach (var s in subscriptions) - { - await _stripeAdapter.SubscriptionCancelAsync(s.Id); - if (s.LatestInvoice?.Status == "open") - { - await _stripeAdapter.InvoiceVoidInvoiceAsync(s.LatestInvoiceId); - } - } - } - - private FileResult StripeSubscriptionsExport(IEnumerable subscriptions) - { - var fieldsToExport = subscriptions.Select(s => new - { - StripeId = s.Id, - CustomerEmail = s.Customer?.Email, - SubscriptionStatus = s.Status, - InvoiceDueDate = s.CurrentPeriodEnd, - SubscriptionProducts = s.Items?.Data.Select(p => p.Plan.Id) - }); - - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - var result = System.Text.Json.JsonSerializer.Serialize(fieldsToExport, options); - var bytes = Encoding.UTF8.GetBytes(result); - return File(bytes, "application/json", "StripeSubscriptionsSearch.json"); - } } diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 14b255b2b6..34d975226e 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -52,8 +52,6 @@ public enum Permission Tools_PromoteProviderServiceUser, Tools_GenerateLicenseFile, Tools_ManageTaxRates, - Tools_ManageStripeSubscriptions, Tools_CreateEditTransaction, - Tools_ProcessStripeEvents, - Tools_MigrateProviders + Tools_ProcessStripeEvents } diff --git a/src/Admin/Models/StripeSubscriptionsModel.cs b/src/Admin/Models/StripeSubscriptionsModel.cs deleted file mode 100644 index 36e1f099e1..0000000000 --- a/src/Admin/Models/StripeSubscriptionsModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; -using Bit.Core.Models.BitStripe; - -namespace Bit.Admin.Models; - -public class StripeSubscriptionRowModel -{ - public Stripe.Subscription Subscription { get; set; } - public bool Selected { get; set; } - - public StripeSubscriptionRowModel() { } - public StripeSubscriptionRowModel(Stripe.Subscription subscription) - { - Subscription = subscription; - } -} - -public enum StripeSubscriptionsAction -{ - Search, - PreviousPage, - NextPage, - Export, - BulkCancel -} - -public class StripeSubscriptionsModel : IValidatableObject -{ - public List Items { get; set; } - public StripeSubscriptionsAction Action { get; set; } = StripeSubscriptionsAction.Search; - public string Message { get; set; } - public List Prices { get; set; } - public List TestClocks { get; set; } - public StripeSubscriptionListOptions Filter { get; set; } = new StripeSubscriptionListOptions(); - public IEnumerable Validate(ValidationContext validationContext) - { - if (Action == StripeSubscriptionsAction.BulkCancel && Filter.Status != "unpaid") - { - yield return new ValidationResult("Bulk cancel is currently only supported for unpaid subscriptions"); - } - } -} diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 5b34e13f6c..5ecbdc899c 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Providers.Migration; #if !OSS using Bit.Commercial.Core.Utilities; @@ -92,7 +91,6 @@ public class Startup services.AddDistributedCache(globalSettings); services.AddBillingOperations(); services.AddHttpClient(); - services.AddProviderMigration(); #if OSS services.AddOosServices(); diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index b60cf895a1..6dddc4ffeb 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -52,8 +52,7 @@ public static class RolePermissionMapping Permission.Tools_PromoteAdmin, Permission.Tools_PromoteProviderServiceUser, Permission.Tools_GenerateLicenseFile, - Permission.Tools_ManageTaxRates, - Permission.Tools_ManageStripeSubscriptions + Permission.Tools_ManageTaxRates } }, { "admin", new List @@ -105,7 +104,6 @@ public static class RolePermissionMapping Permission.Tools_PromoteProviderServiceUser, Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, - Permission.Tools_ManageStripeSubscriptions, Permission.Tools_CreateEditTransaction } }, @@ -180,10 +178,8 @@ public static class RolePermissionMapping Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, - Permission.Tools_ManageStripeSubscriptions, Permission.Tools_CreateEditTransaction, - Permission.Tools_ProcessStripeEvents, - Permission.Tools_MigrateProviders + Permission.Tools_ProcessStripeEvents } }, { "sales", new List diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 1661a8bbc3..c13be428b4 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -13,12 +13,10 @@ var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin); var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser); var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); - var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); - var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders); var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser || - canGenerateLicense || canManageStripeSubscriptions; + canGenerateLicense; } @@ -102,12 +100,6 @@ Generate License - } - @if (canManageStripeSubscriptions) - { - - Manage Stripe Subscriptions - } @if (canProcessStripeEvents) { @@ -115,12 +107,6 @@ Process Stripe Events } - @if (canMigrateProviders) - { - - Migrate Providers - - } } diff --git a/src/Admin/Views/Tools/StripeSubscriptions.cshtml b/src/Admin/Views/Tools/StripeSubscriptions.cshtml deleted file mode 100644 index d8c168b3b0..0000000000 --- a/src/Admin/Views/Tools/StripeSubscriptions.cshtml +++ /dev/null @@ -1,277 +0,0 @@ -@model StripeSubscriptionsModel -@{ - ViewData["Title"] = "Stripe Subscriptions"; -} - -@section Scripts { - -} - -

Manage Stripe Subscriptions

-@if (!string.IsNullOrWhiteSpace(Model.Message)) -{ -
-} -
-
-
-
- - -
-
- -
-
-
- - -
-
- - -
-
- @{ - var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty; - } - -
-
-
- - -
-
- - -
-
- -
-
-
- -
-
- All @Model.Items.Count subscriptions on this page are selected.
- - - All subscriptions for this search are selected. - - -
-
-
- - - - - - - - - - - - - @if (!Model.Items.Any()) - { - - - - } - else - { - @for (var i = 0; i < Model.Items.Count; i++) - { - - - - - - - - - } - } - -
-
- -
-
IdCustomer EmailStatusProduct TierCurrent Period End
No results to list.
- - @{ - var i0 = i; - } - - - - - - - - @for (var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++) - { - var i1 = i; - var j1 = j; - - } -
- - @{ - var i2 = i; - } - -
-
- @Model.Items[i].Subscription.Id - - @Model.Items[i].Subscription.Customer?.Email - - @Model.Items[i].Subscription.Status - - @string.Join(",", Model.Items[i].Subscription.Items.Data.Select(product => product.Plan.Id).ToArray()) - - @Model.Items[i].Subscription.CurrentPeriodEnd.ToShortDateString() -
-
- -
diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index f7d0593812..006a7ce068 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -132,7 +132,7 @@ public class ProviderBillingController( } var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, - new SubscriptionGetOptions { Expand = ["customer.tax_ids", "test_clock"] }); + new SubscriptionGetOptions { Expand = ["customer.tax_ids", "discounts", "test_clock"] }); var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index e5b868af9a..4b78127240 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Tax.Models; @@ -10,7 +11,7 @@ namespace Bit.Api.Billing.Models.Responses; public record ProviderSubscriptionResponse( string Status, - DateTime CurrentPeriodEndDate, + DateTime? CurrentPeriodEndDate, decimal? DiscountPercentage, string CollectionMethod, IEnumerable Plans, @@ -51,10 +52,12 @@ public record ProviderSubscriptionResponse( var accountCredit = Convert.ToDecimal(subscription.Customer?.Balance) * -1 / 100; + var discount = subscription.Customer?.Discount ?? subscription.Discounts?.FirstOrDefault(); + return new ProviderSubscriptionResponse( subscription.Status, - subscription.CurrentPeriodEnd, - subscription.Customer?.Discount?.Coupon?.PercentOff, + subscription.GetCurrentPeriodEnd(), + discount?.Coupon?.PercentOff, subscription.CollectionMethod, providerPlanResponses, accountCredit, diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 3dc3e3e808..fc38f8fe60 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -7,9 +7,7 @@ public class BillingSettings { public virtual string JobsKey { get; set; } public virtual string StripeWebhookKey { get; set; } - public virtual string StripeWebhookSecret { get; set; } - public virtual string StripeWebhookSecret20231016 { get; set; } - public virtual string StripeWebhookSecret20240620 { get; set; } + public virtual string StripeWebhookSecret20250827Basil { get; set; } public virtual string BitPayWebhookKey { get; set; } public virtual string AppleWebhookKey { get; set; } public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings(); diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index b60e0c56e4..18f2198119 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -120,9 +120,7 @@ public class StripeController : Controller return deliveryContainer.ApiVersion switch { - "2024-06-20" => HandleVersionWith(_billingSettings.StripeWebhookSecret20240620), - "2023-10-16" => HandleVersionWith(_billingSettings.StripeWebhookSecret20231016), - "2022-08-01" => HandleVersionWith(_billingSettings.StripeWebhookSecret), + "2025-08-27.basil" => HandleVersionWith(_billingSettings.StripeWebhookSecret20250827Basil), _ => HandleDefault(deliveryContainer.ApiVersion) }; diff --git a/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs b/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs index 5bb098bec5..101b0e26b9 100644 --- a/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs +++ b/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs @@ -1,4 +1,5 @@ -using Event = Stripe.Event; +using Bit.Core.Billing.Constants; +using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -35,13 +36,13 @@ public class InvoiceCreatedHandler( if (usingPayPal && invoice is { AmountDue: > 0, - Paid: false, + Status: not StripeConstants.InvoiceStatus.Paid, CollectionMethod: "charge_automatically", BillingReason: "subscription_create" or "subscription_cycle" or "automatic_pending_invoice_item_invoice", - SubscriptionId: not null and not "" + Parent.SubscriptionDetails: not null }) { await stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice); diff --git a/src/Billing/Services/Implementations/PaymentFailedHandler.cs b/src/Billing/Services/Implementations/PaymentFailedHandler.cs index acf6ca70c7..0da6d03e94 100644 --- a/src/Billing/Services/Implementations/PaymentFailedHandler.cs +++ b/src/Billing/Services/Implementations/PaymentFailedHandler.cs @@ -1,4 +1,5 @@ -using Stripe; +using Bit.Core.Billing.Constants; +using Stripe; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -26,17 +27,20 @@ public class PaymentFailedHandler : IPaymentFailedHandler public async Task HandleAsync(Event parsedEvent) { var invoice = await _stripeEventService.GetInvoice(parsedEvent, true); - if (invoice.Paid || invoice.AttemptCount <= 1 || !ShouldAttemptToPayInvoice(invoice)) + if (invoice.Status == StripeConstants.InvoiceStatus.Paid || invoice.AttemptCount <= 1 || !ShouldAttemptToPayInvoice(invoice)) { return; } - var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); - // attempt count 4 = 11 days after initial failure - if (invoice.AttemptCount <= 3 || - !subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore)) + if (invoice.Parent?.SubscriptionDetails != null) { - await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice); + var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId); + // attempt count 4 = 11 days after initial failure + if (invoice.AttemptCount <= 3 || + !subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore)) + { + await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice); + } } } @@ -44,9 +48,9 @@ public class PaymentFailedHandler : IPaymentFailedHandler invoice is { AmountDue: > 0, - Paid: false, + Status: not StripeConstants.InvoiceStatus.Paid, CollectionMethod: "charge_automatically", BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice", - SubscriptionId: not null + Parent.SubscriptionDetails: not null }; } diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index a10fa4b3d6..443227f7bf 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -1,7 +1,9 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Repositories; using Bit.Core.Services; @@ -29,12 +31,17 @@ public class PaymentSucceededHandler( public async Task HandleAsync(Event parsedEvent) { var invoice = await stripeEventService.GetInvoice(parsedEvent, true); - if (!invoice.Paid || invoice.BillingReason != "subscription_create") + if (invoice.Status != StripeConstants.InvoiceStatus.Paid || invoice.BillingReason != "subscription_create") { return; } - var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId); + if (invoice.Parent?.SubscriptionDetails == null) + { + return; + } + + var subscription = await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId); if (subscription?.Status != StripeSubscriptionStatus.Active) { return; @@ -96,7 +103,7 @@ public class PaymentSucceededHandler( return; } - await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd()); organization = await organizationRepository.GetByIdAsync(organization.Id); await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!); } @@ -107,7 +114,7 @@ public class PaymentSucceededHandler( return; } - await userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); + await userService.EnablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd()); } } } diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 12716c5aa2..79c85cb48f 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -28,9 +28,14 @@ public class ProviderEventService( return; } - var invoice = await stripeEventService.GetInvoice(parsedEvent); + var invoice = await stripeEventService.GetInvoice(parsedEvent, true, ["discounts"]); - var metadata = (await stripeFacade.GetSubscription(invoice.SubscriptionId)).Metadata ?? new Dictionary(); + if (invoice.Parent is not { Type: "subscription_details" }) + { + return; + } + + var metadata = (await stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId)).Metadata ?? new Dictionary(); var hasProviderId = metadata.TryGetValue("providerId", out var providerId); @@ -68,7 +73,9 @@ public class ProviderEventService( var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); - var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; + var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0; + + var discountedPercentage = (100 - totalPercentOff) / 100; var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; @@ -96,7 +103,9 @@ public class ProviderEventService( var unassignedSeats = providerPlan.SeatMinimum - clientSeats ?? 0; - var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100; + var totalPercentOff = invoice.Discounts?.Sum(discount => discount?.Coupon?.PercentOff ?? 0) ?? 0; + + var discountedPercentage = (100 - totalPercentOff) / 100; var discountedSeatPrice = plan.PasswordManager.ProviderPortalSeatPrice * discountedPercentage; diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index 4c96bf977d..49e562de56 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -2,6 +2,7 @@ #nullable disable using Bit.Billing.Constants; +using Bit.Core.Billing.Constants; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -87,25 +88,6 @@ public class StripeEventUtilityService : IStripeEventUtilityService /// public async Task<(Guid?, Guid?, Guid?)> GetEntityIdsFromChargeAsync(Charge charge) { - Guid? organizationId = null; - Guid? userId = null; - Guid? providerId = null; - - if (charge.InvoiceId != null) - { - var invoice = await _stripeFacade.GetInvoice(charge.InvoiceId); - if (invoice?.SubscriptionId != null) - { - var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); - (organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata); - } - } - - if (organizationId.HasValue || userId.HasValue || providerId.HasValue) - { - return (organizationId, userId, providerId); - } - var subscriptions = await _stripeFacade.ListSubscriptions(new SubscriptionListOptions { Customer = charge.CustomerId @@ -118,7 +100,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService continue; } - (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata); + var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription.Metadata); if (organizationId.HasValue || userId.HasValue || providerId.HasValue) { @@ -256,10 +238,10 @@ public class StripeEventUtilityService : IStripeEventUtilityService invoice is { AmountDue: > 0, - Paid: false, + Status: not StripeConstants.InvoiceStatus.Paid, CollectionMethod: "charge_automatically", BillingReason: "subscription_cycle" or "automatic_pending_invoice_item_invoice", - SubscriptionId: not null + Parent.SubscriptionDetails: not null }; private async Task AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer) @@ -272,7 +254,13 @@ public class StripeEventUtilityService : IStripeEventUtilityService return false; } - var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); + if (invoice.Parent?.SubscriptionDetails == null) + { + _logger.LogWarning("Invoice parent was not a subscription."); + return false; + } + + var subscription = await _stripeFacade.GetSubscription(invoice.Parent.SubscriptionDetails.SubscriptionId); var (organizationId, userId, providerId) = GetIdsFromMetadata(subscription?.Metadata); if (!organizationId.HasValue && !userId.HasValue && !providerId.HasValue) { diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 465da86c3f..13adf9825d 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -1,5 +1,6 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Billing.Extensions; using Bit.Core.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -50,11 +51,11 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler return; } - await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd()); } else if (userId.HasValue) { - await _userService.DisablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); + await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd()); } } } diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 10630f78f4..81aeb460c2 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -5,6 +5,8 @@ using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -82,12 +84,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice", "test_clock"]); var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); + var currentPeriodEnd = subscription.GetCurrentPeriodEnd(); + switch (subscription.Status) { case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired when organizationId.HasValue: { - await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, currentPeriodEnd); if (subscription.Status == StripeSubscriptionStatus.Unpaid && subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" }) { @@ -114,7 +118,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler await VoidOpenInvoices(subscription.Id); } - await _userService.DisablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); + await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd); break; } @@ -154,7 +158,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler { if (userId.HasValue) { - await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); + await _userService.EnablePremiumAsync(userId.Value, currentPeriodEnd); } break; } @@ -162,17 +166,17 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler if (organizationId.HasValue) { - await _organizationService.UpdateExpirationDateAsync(organizationId.Value, subscription.CurrentPeriodEnd); - if (_stripeEventUtilityService.IsSponsoredSubscription(subscription)) + await _organizationService.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd); + if (_stripeEventUtilityService.IsSponsoredSubscription(subscription) && currentPeriodEnd.HasValue) { - await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(organizationId.Value, currentPeriodEnd.Value); } await RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(parsedEvent, subscription); } else if (userId.HasValue) { - await _userService.UpdatePremiumExpirationAsync(userId.Value, subscription.CurrentPeriodEnd); + await _userService.UpdatePremiumExpirationAsync(userId.Value, currentPeriodEnd); } } @@ -280,9 +284,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler ?.Coupon ?.Id == "sm-standalone"; - var subscriptionHasSecretsManagerTrial = subscription.Discount - ?.Coupon - ?.Id == "sm-standalone"; + var subscriptionHasSecretsManagerTrial = subscription.Discounts.Select(discount => discount.Coupon.Id) + .Contains(StripeConstants.CouponIDs.SecretsManagerStandalone); if (customerHasSecretsManagerTrial) { diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index e5675f7c0a..4260d67dfa 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -36,17 +36,16 @@ public class UpcomingInvoiceHandler( { var invoice = await stripeEventService.GetInvoice(parsedEvent); - if (string.IsNullOrEmpty(invoice.SubscriptionId)) + var customer = + await stripeFacade.GetCustomer(invoice.CustomerId, new CustomerGetOptions { Expand = ["subscriptions", "tax", "tax_ids"] }); + + var subscription = customer.Subscriptions.FirstOrDefault(); + + if (subscription == null) { - logger.LogInformation("Received 'invoice.upcoming' Event with ID '{eventId}' that did not include a Subscription ID", parsedEvent.Id); return; } - var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId, new SubscriptionGetOptions - { - Expand = ["customer.tax", "customer.tax_ids"] - }); - var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); if (organizationId.HasValue) @@ -58,7 +57,7 @@ public class UpcomingInvoiceHandler( return; } - await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id); + await AlignOrganizationTaxConcernsAsync(organization, subscription, customer, parsedEvent.Id); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -137,7 +136,7 @@ public class UpcomingInvoiceHandler( return; } - await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id); + await AlignProviderTaxConcernsAsync(provider, subscription, customer, parsedEvent.Id); await SendProviderUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice, subscription, providerId.Value); } @@ -199,13 +198,14 @@ public class UpcomingInvoiceHandler( private async Task AlignOrganizationTaxConcernsAsync( Organization organization, Subscription subscription, + Customer customer, string eventId) { var nonUSBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families && - subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates; + customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates; - if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + if (nonUSBusinessUse && customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { try { @@ -246,10 +246,11 @@ public class UpcomingInvoiceHandler( private async Task AlignProviderTaxConcernsAsync( Provider provider, Subscription subscription, + Customer customer, string eventId) { - if (subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates && - subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + if (customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates && + customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { try { diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 6c90c22686..a2d6acd0a1 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -57,9 +57,7 @@ "billingSettings": { "jobsKey": "SECRET", "stripeWebhookKey": "SECRET", - "stripeWebhookSecret": "SECRET", - "stripeWebhookSecret20231016": "SECRET", - "stripeWebhookSecret20240620": "SECRET", + "stripeWebhookSecret20250827Basil": "SECRET", "bitPayWebhookKey": "SECRET", "appleWebhookKey": "SECRET", "payPal": { @@ -87,6 +85,6 @@ "runSearch": "always", "realTime": true } - } + } } } diff --git a/src/Core/Billing/Extensions/InvoiceExtensions.cs b/src/Core/Billing/Extensions/InvoiceExtensions.cs index bb9f7588bf..d62959c09a 100644 --- a/src/Core/Billing/Extensions/InvoiceExtensions.cs +++ b/src/Core/Billing/Extensions/InvoiceExtensions.cs @@ -64,10 +64,12 @@ public static class InvoiceExtensions } } + var tax = invoice.TotalTaxes?.Sum(invoiceTotalTax => invoiceTotalTax.Amount) ?? 0; + // Add fallback tax from invoice-level tax if present and not already included - if (invoice.Tax.HasValue && invoice.Tax.Value > 0) + if (tax > 0) { - var taxAmount = invoice.Tax.Value / 100m; + var taxAmount = tax / 100m; items.Add($"1 × Tax (at ${taxAmount:F2} / month)"); } diff --git a/src/Core/Billing/Extensions/SubscriptionExtensions.cs b/src/Core/Billing/Extensions/SubscriptionExtensions.cs new file mode 100644 index 0000000000..383bd32d53 --- /dev/null +++ b/src/Core/Billing/Extensions/SubscriptionExtensions.cs @@ -0,0 +1,25 @@ +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class SubscriptionExtensions +{ + /* + * For the time being, this is the simplest migration approach from v45 to v48 as + * we do not support multi-cadence subscriptions. Each subscription item should be on the + * same billing cycle. If this changes, we'll need a significantly more robust approach. + * + * Because we can't guarantee a subscription will have items, this has to be nullable. + */ + public static (DateTime? Start, DateTime? End)? GetCurrentPeriod(this Subscription subscription) + { + var item = subscription.Items?.FirstOrDefault(); + return item is null ? null : (item.CurrentPeriodStart, item.CurrentPeriodEnd); + } + + public static DateTime? GetCurrentPeriodStart(this Subscription subscription) => + subscription.Items?.FirstOrDefault()?.CurrentPeriodStart; + + public static DateTime? GetCurrentPeriodEnd(this Subscription subscription) => + subscription.Items?.FirstOrDefault()?.CurrentPeriodEnd; +} diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs deleted file mode 100644 index d00b5b46a4..0000000000 --- a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class UpcomingInvoiceOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given upcoming invoice options. - /// - /// - /// The existing customer to which the upcoming invoice belongs. - /// The existing subscription to which the upcoming invoice belongs. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax( - this UpcomingInvoiceOptions options, - Customer customer, - Subscription subscription) - { - if (subscription != null && subscription.AutomaticTax.Enabled) - { - return false; - } - - // We might only need to check the automatic tax status. - if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - options.SubscriptionDefaultTaxRates = []; - - return true; - } -} diff --git a/src/Core/Billing/Models/BillingHistoryInfo.cs b/src/Core/Billing/Models/BillingHistoryInfo.cs index 3114e22fdf..0f3d07844c 100644 --- a/src/Core/Billing/Models/BillingHistoryInfo.cs +++ b/src/Core/Billing/Models/BillingHistoryInfo.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Billing.Constants; using Bit.Core.Entities; using Bit.Core.Enums; using Stripe; @@ -46,7 +47,7 @@ public class BillingHistoryInfo Url = inv.HostedInvoiceUrl; PdfUrl = inv.InvoicePdf; Number = inv.Number; - Paid = inv.Paid; + Paid = inv.Status == StripeConstants.InvoiceStatus.Paid; Amount = inv.Total / 100M; } diff --git a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs index 77bbe655c4..89d301c22a 100644 --- a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs +++ b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs @@ -75,7 +75,13 @@ public class PreviewOrganizationTaxCommand( Quantity = purchase.SecretsManager.Seats } ]); - options.Coupon = CouponIDs.SecretsManagerStandalone; + options.Discounts = + [ + new InvoiceDiscountOptions + { + Coupon = CouponIDs.SecretsManagerStandalone + } + ]; break; default: @@ -180,7 +186,10 @@ public class PreviewOrganizationTaxCommand( if (subscription.Customer.Discount != null) { - options.Coupon = subscription.Customer.Discount.Coupon.Id; + options.Discounts = + [ + new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id } + ]; } var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -277,7 +286,10 @@ public class PreviewOrganizationTaxCommand( if (subscription.Customer.Discount != null) { - options.Coupon = subscription.Customer.Discount.Coupon.Id; + options.Discounts = + [ + new InvoiceDiscountOptions { Coupon = subscription.Customer.Discount.Coupon.Id } + ]; } var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -329,7 +341,7 @@ public class PreviewOrganizationTaxCommand( }); private static (decimal, decimal) GetAmounts(Invoice invoice) => ( - Convert.ToDecimal(invoice.Tax) / 100, + Convert.ToDecimal(invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100, Convert.ToDecimal(invoice.Total) / 100); private static InvoiceCreatePreviewOptions GetBaseOptions( diff --git a/src/Core/Billing/Organizations/Models/OrganizationSale.cs b/src/Core/Billing/Organizations/Models/OrganizationSale.cs index f1f3a636b7..a984d5fe71 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationSale.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationSale.cs @@ -9,7 +9,7 @@ namespace Bit.Core.Billing.Organizations.Models; public class OrganizationSale { - private OrganizationSale() { } + internal OrganizationSale() { } public void Deconstruct( out Organization organization, diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index 887a6badf5..01e520ea41 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -162,17 +162,23 @@ public class GetOrganizationWarningsQuery( if (subscription is { Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active, - LatestInvoice: null or { Status: InvoiceStatus.Paid } - } && (subscription.CurrentPeriodEnd - now).TotalDays <= 14) + LatestInvoice: null or { Status: InvoiceStatus.Paid }, + Items.Data.Count: > 0 + }) { - return new ResellerRenewalWarning + var currentPeriodEnd = subscription.GetCurrentPeriodEnd(); + + if (currentPeriodEnd != null && (currentPeriodEnd.Value - now).TotalDays <= 14) { - Type = "upcoming", - Upcoming = new ResellerRenewalWarning.UpcomingRenewal + return new ResellerRenewalWarning { - RenewalDate = subscription.CurrentPeriodEnd - } - }; + Type = "upcoming", + Upcoming = new ResellerRenewalWarning.UpcomingRenewal + { + RenewalDate = currentPeriodEnd.Value + } + }; + } } if (subscription is diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 9af9cdb345..2381bdda96 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -45,12 +45,12 @@ public class OrganizationBillingService( ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType) : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup); - var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup); + var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup, customerSetup?.Coupon); if (subscription.Status is StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active) { organization.Enabled = true; - organization.ExpirationDate = subscription.CurrentPeriodEnd; + organization.ExpirationDate = subscription.GetCurrentPeriodEnd(); await organizationRepository.ReplaceAsync(organization); } } @@ -187,7 +187,6 @@ public class OrganizationBillingService( var customerCreateOptions = new CustomerCreateOptions { - Coupon = customerSetup.Coupon, Description = organization.DisplayBusinessName(), Email = organization.BillingEmail, Expand = ["tax", "tax_ids"], @@ -273,7 +272,7 @@ public class OrganizationBillingService( customerCreateOptions.TaxIdData = [ - new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } + new CustomerTaxIdDataOptions { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } ]; if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) @@ -381,7 +380,8 @@ public class OrganizationBillingService( private async Task CreateSubscriptionAsync( Organization organization, Customer customer, - SubscriptionSetup subscriptionSetup) + SubscriptionSetup subscriptionSetup, + string? coupon) { var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType); @@ -444,6 +444,7 @@ public class OrganizationBillingService( { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, + Discounts = !string.IsNullOrEmpty(coupon) ? [new SubscriptionDiscountOptions { Coupon = coupon }] : null, Items = subscriptionItemOptionsList, Metadata = new Dictionary { @@ -459,8 +460,9 @@ public class OrganizationBillingService( var hasPaymentMethod = await hasPaymentMethodQuery.Run(organization); - // Only set trial_settings.end_behavior.missing_payment_method to "cancel" if there is no payment method - if (!hasPaymentMethod) + // Only set trial_settings.end_behavior.missing_payment_method to "cancel" + // if there is no payment method AND there's an actual trial period + if (!hasPaymentMethod && subscriptionCreateOptions.TrialPeriodDays > 0) { subscriptionCreateOptions.TrialSettings = new SubscriptionTrialSettingsOptions { diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs index 1227cdc034..c5fdc3287a 100644 --- a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Services; using Bit.Core.Entities; @@ -87,7 +88,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand( when subscription.Status == StripeConstants.SubscriptionStatus.Active: { user.Premium = true; - user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); break; } } diff --git a/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs index a0b4fcabc2..9275bcf3d9 100644 --- a/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs +++ b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs @@ -60,6 +60,6 @@ public class PreviewPremiumTaxCommand( }); private static (decimal, decimal) GetAmounts(Invoice invoice) => ( - Convert.ToDecimal(invoice.Tax) / 100, + Convert.ToDecimal(invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount)) / 100, Convert.ToDecimal(invoice.Total) / 100); } diff --git a/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs deleted file mode 100644 index 65fd7726f8..0000000000 --- a/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs +++ /dev/null @@ -1,26 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -namespace Bit.Core.Billing.Providers.Migration.Models; - -public enum ClientMigrationProgress -{ - Started = 1, - MigrationRecordCreated = 2, - SubscriptionEnded = 3, - Completed = 4, - - Reversing = 5, - ResetOrganization = 6, - RecreatedSubscription = 7, - RemovedMigrationRecord = 8, - Reversed = 9 -} - -public class ClientMigrationTracker -{ - public Guid ProviderId { get; set; } - public Guid OrganizationId { get; set; } - public string OrganizationName { get; set; } - public ClientMigrationProgress Progress { get; set; } = ClientMigrationProgress.Started; -} diff --git a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs deleted file mode 100644 index 78a2631999..0000000000 --- a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs +++ /dev/null @@ -1,48 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.Billing.Providers.Entities; - -namespace Bit.Core.Billing.Providers.Migration.Models; - -public class ProviderMigrationResult -{ - public Guid ProviderId { get; set; } - public string ProviderName { get; set; } - public string Result { get; set; } - public List Clients { get; set; } -} - -public class ClientMigrationResult -{ - public Guid OrganizationId { get; set; } - public string OrganizationName { get; set; } - public string Result { get; set; } - public ClientPreviousState PreviousState { get; set; } -} - -public class ClientPreviousState -{ - public ClientPreviousState() { } - - public ClientPreviousState(ClientOrganizationMigrationRecord migrationRecord) - { - PlanType = migrationRecord.PlanType.ToString(); - Seats = migrationRecord.Seats; - MaxStorageGb = migrationRecord.MaxStorageGb; - GatewayCustomerId = migrationRecord.GatewayCustomerId; - GatewaySubscriptionId = migrationRecord.GatewaySubscriptionId; - ExpirationDate = migrationRecord.ExpirationDate; - MaxAutoscaleSeats = migrationRecord.MaxAutoscaleSeats; - Status = migrationRecord.Status.ToString(); - } - - public string PlanType { get; set; } - public int Seats { get; set; } - public short? MaxStorageGb { get; set; } - public string GatewayCustomerId { get; set; } = null!; - public string GatewaySubscriptionId { get; set; } = null!; - public DateTime? ExpirationDate { get; set; } - public int? MaxAutoscaleSeats { get; set; } - public string Status { get; set; } -} diff --git a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs deleted file mode 100644 index ba39feab2d..0000000000 --- a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs +++ /dev/null @@ -1,25 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -namespace Bit.Core.Billing.Providers.Migration.Models; - -public enum ProviderMigrationProgress -{ - Started = 1, - NoClients = 2, - ClientsMigrated = 3, - TeamsPlanConfigured = 4, - EnterprisePlanConfigured = 5, - CustomerSetup = 6, - SubscriptionSetup = 7, - CreditApplied = 8, - Completed = 9, -} - -public class ProviderMigrationTracker -{ - public Guid ProviderId { get; set; } - public string ProviderName { get; set; } - public List OrganizationIds { get; set; } - public ProviderMigrationProgress Progress { get; set; } = ProviderMigrationProgress.Started; -} diff --git a/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs b/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs deleted file mode 100644 index 1061c82888..0000000000 --- a/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.Core.Billing.Providers.Migration.Services; -using Bit.Core.Billing.Providers.Migration.Services.Implementations; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Billing.Providers.Migration; - -public static class ServiceCollectionExtensions -{ - public static void AddProviderMigration(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - } -} diff --git a/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs b/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs deleted file mode 100644 index 70649590df..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Providers.Migration.Models; - -namespace Bit.Core.Billing.Providers.Migration.Services; - -public interface IMigrationTrackerCache -{ - Task StartTracker(Provider provider); - Task SetOrganizationIds(Guid providerId, IEnumerable organizationIds); - Task GetTracker(Guid providerId); - Task UpdateTrackingStatus(Guid providerId, ProviderMigrationProgress status); - - Task StartTracker(Guid providerId, Organization organization); - Task GetTracker(Guid providerId, Guid organizationId); - Task UpdateTrackingStatus(Guid providerId, Guid organizationId, ClientMigrationProgress status); -} diff --git a/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs deleted file mode 100644 index a0548277b4..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Bit.Core.AdminConsole.Entities; - -namespace Bit.Core.Billing.Providers.Migration.Services; - -public interface IOrganizationMigrator -{ - Task Migrate(Guid providerId, Organization organization); -} diff --git a/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs deleted file mode 100644 index 328c2419f4..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Bit.Core.Billing.Providers.Migration.Models; - -namespace Bit.Core.Billing.Providers.Migration.Services; - -public interface IProviderMigrator -{ - Task Migrate(Guid providerId); - - Task GetResult(Guid providerId); -} diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs deleted file mode 100644 index 1f38b0d111..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs +++ /dev/null @@ -1,110 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Providers.Migration.Models; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; - -public class MigrationTrackerDistributedCache( - [FromKeyedServices("persistent")] - IDistributedCache distributedCache) : IMigrationTrackerCache -{ - public async Task StartTracker(Provider provider) => - await SetAsync(new ProviderMigrationTracker - { - ProviderId = provider.Id, - ProviderName = provider.Name - }); - - public async Task SetOrganizationIds(Guid providerId, IEnumerable organizationIds) - { - var tracker = await GetAsync(providerId); - - tracker.OrganizationIds = organizationIds.ToList(); - - await SetAsync(tracker); - } - - public Task GetTracker(Guid providerId) => GetAsync(providerId); - - public async Task UpdateTrackingStatus(Guid providerId, ProviderMigrationProgress status) - { - var tracker = await GetAsync(providerId); - - tracker.Progress = status; - - await SetAsync(tracker); - } - - public async Task StartTracker(Guid providerId, Organization organization) => - await SetAsync(new ClientMigrationTracker - { - ProviderId = providerId, - OrganizationId = organization.Id, - OrganizationName = organization.Name - }); - - public Task GetTracker(Guid providerId, Guid organizationId) => - GetAsync(providerId, organizationId); - - public async Task UpdateTrackingStatus(Guid providerId, Guid organizationId, ClientMigrationProgress status) - { - var tracker = await GetAsync(providerId, organizationId); - - tracker.Progress = status; - - await SetAsync(tracker); - } - - private static string GetProviderCacheKey(Guid providerId) => $"provider_{providerId}_migration"; - - private static string GetClientCacheKey(Guid providerId, Guid clientId) => - $"provider_{providerId}_client_{clientId}_migration"; - - private async Task GetAsync(Guid providerId) - { - var cacheKey = GetProviderCacheKey(providerId); - - var json = await distributedCache.GetStringAsync(cacheKey); - - return string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize(json); - } - - private async Task GetAsync(Guid providerId, Guid organizationId) - { - var cacheKey = GetClientCacheKey(providerId, organizationId); - - var json = await distributedCache.GetStringAsync(cacheKey); - - return string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize(json); - } - - private async Task SetAsync(ProviderMigrationTracker tracker) - { - var cacheKey = GetProviderCacheKey(tracker.ProviderId); - - var json = JsonSerializer.Serialize(tracker); - - await distributedCache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromMinutes(30) - }); - } - - private async Task SetAsync(ClientMigrationTracker tracker) - { - var cacheKey = GetClientCacheKey(tracker.ProviderId, tracker.OrganizationId); - - var json = JsonSerializer.Serialize(tracker); - - await distributedCache.SetStringAsync(cacheKey, json, new DistributedCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromMinutes(30) - }); - } -} diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs deleted file mode 100644 index 3de49838af..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs +++ /dev/null @@ -1,331 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Providers.Entities; -using Bit.Core.Billing.Providers.Migration.Models; -using Bit.Core.Billing.Providers.Repositories; -using Bit.Core.Enums; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.Extensions.Logging; -using Stripe; -using Plan = Bit.Core.Models.StaticStore.Plan; - -namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; - -public class OrganizationMigrator( - IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, - ILogger logger, - IMigrationTrackerCache migrationTrackerCache, - IOrganizationRepository organizationRepository, - IPricingClient pricingClient, - IStripeAdapter stripeAdapter) : IOrganizationMigrator -{ - private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing"; - - public async Task Migrate(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Starting migration for organization ({OrganizationID})", organization.Id); - - await migrationTrackerCache.StartTracker(providerId, organization); - - await CreateMigrationRecordAsync(providerId, organization); - - await CancelSubscriptionAsync(providerId, organization); - - await UpdateOrganizationAsync(providerId, organization); - } - - #region Steps - - private async Task CreateMigrationRecordAsync(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Creating ClientOrganizationMigrationRecord for organization ({OrganizationID})", organization.Id); - - var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); - - if (migrationRecord != null) - { - logger.LogInformation( - "CB: ClientOrganizationMigrationRecord already exists for organization ({OrganizationID}), deleting record", - organization.Id); - - await clientOrganizationMigrationRecordRepository.DeleteAsync(migrationRecord); - } - - await clientOrganizationMigrationRecordRepository.CreateAsync(new ClientOrganizationMigrationRecord - { - OrganizationId = organization.Id, - ProviderId = providerId, - PlanType = organization.PlanType, - Seats = organization.Seats ?? 0, - MaxStorageGb = organization.MaxStorageGb, - GatewayCustomerId = organization.GatewayCustomerId!, - GatewaySubscriptionId = organization.GatewaySubscriptionId!, - ExpirationDate = organization.ExpirationDate, - MaxAutoscaleSeats = organization.MaxAutoscaleSeats, - Status = organization.Status - }); - - logger.LogInformation("CB: Created migration record for organization ({OrganizationID})", organization.Id); - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, - ClientMigrationProgress.MigrationRecordCreated); - } - - private async Task CancelSubscriptionAsync(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Cancelling subscription for organization ({OrganizationID})", organization.Id); - - var subscription = await stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId); - - if (subscription is - { - Status: - StripeConstants.SubscriptionStatus.Active or - StripeConstants.SubscriptionStatus.PastDue or - StripeConstants.SubscriptionStatus.Trialing - }) - { - await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, - new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); - - subscription = await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId, - new SubscriptionCancelOptions - { - CancellationDetails = new SubscriptionCancellationDetailsOptions - { - Comment = _cancellationComment - }, - InvoiceNow = true, - Prorate = true, - Expand = ["latest_invoice", "test_clock"] - }); - - logger.LogInformation("CB: Cancelled subscription for organization ({OrganizationID})", organization.Id); - - var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; - - var trialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now; - - if (!trialing && subscription is { Status: StripeConstants.SubscriptionStatus.Canceled, CancellationDetails.Comment: _cancellationComment }) - { - var latestInvoice = subscription.LatestInvoice; - - if (latestInvoice.Status == "draft") - { - await stripeAdapter.InvoiceFinalizeInvoiceAsync(latestInvoice.Id, - new InvoiceFinalizeOptions { AutoAdvance = true }); - - logger.LogInformation("CB: Finalized prorated invoice for organization ({OrganizationID})", organization.Id); - } - } - } - else - { - logger.LogInformation( - "CB: Did not need to cancel subscription for organization ({OrganizationID}) as it was inactive", - organization.Id); - } - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, - ClientMigrationProgress.SubscriptionEnded); - } - - private async Task UpdateOrganizationAsync(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management", - organization.Id); - - var plan = await pricingClient.GetPlanOrThrow(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly); - - ResetOrganizationPlan(organization, plan); - organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; - organization.GatewaySubscriptionId = null; - organization.ExpirationDate = null; - organization.MaxAutoscaleSeats = null; - organization.Status = OrganizationStatusType.Managed; - - await organizationRepository.ReplaceAsync(organization); - - logger.LogInformation("CB: Brought organization ({OrganizationID}) under provider management", - organization.Id); - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, - ClientMigrationProgress.Completed); - } - - #endregion - - #region Reverse - - private async Task RemoveMigrationRecordAsync(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Removing migration record for organization ({OrganizationID})", organization.Id); - - var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); - - if (migrationRecord != null) - { - await clientOrganizationMigrationRecordRepository.DeleteAsync(migrationRecord); - - logger.LogInformation( - "CB: Removed migration record for organization ({OrganizationID})", - organization.Id); - } - else - { - logger.LogInformation("CB: Did not remove migration record for organization ({OrganizationID}) as it does not exist", organization.Id); - } - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, ClientMigrationProgress.Reversed); - } - - private async Task RecreateSubscriptionAsync(Guid providerId, Organization organization) - { - logger.LogInformation("CB: Recreating subscription for organization ({OrganizationID})", organization.Id); - - if (!string.IsNullOrEmpty(organization.GatewaySubscriptionId)) - { - if (string.IsNullOrEmpty(organization.GatewayCustomerId)) - { - logger.LogError( - "CB: Cannot recreate subscription for organization ({OrganizationID}) as it does not have a Stripe customer", - organization.Id); - - throw new Exception(); - } - - var customer = await stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, - new CustomerGetOptions { Expand = ["default_source", "invoice_settings.default_payment_method"] }); - - var collectionMethod = - customer.DefaultSource != null || - customer.InvoiceSettings?.DefaultPaymentMethod != null || - customer.Metadata.ContainsKey(Utilities.BraintreeCustomerIdKey) - ? StripeConstants.CollectionMethod.ChargeAutomatically - : StripeConstants.CollectionMethod.SendInvoice; - - var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); - - var items = new List - { - new () - { - Price = plan.PasswordManager.StripeSeatPlanId, - Quantity = organization.Seats - } - }; - - if (organization.MaxStorageGb.HasValue && plan.PasswordManager.BaseStorageGb.HasValue && organization.MaxStorageGb.Value > plan.PasswordManager.BaseStorageGb.Value) - { - var additionalStorage = organization.MaxStorageGb.Value - plan.PasswordManager.BaseStorageGb.Value; - - items.Add(new SubscriptionItemOptions - { - Price = plan.PasswordManager.StripeStoragePlanId, - Quantity = additionalStorage - }); - } - - var subscriptionCreateOptions = new SubscriptionCreateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }, - Customer = customer.Id, - CollectionMethod = collectionMethod, - DaysUntilDue = collectionMethod == StripeConstants.CollectionMethod.SendInvoice ? 30 : null, - Items = items, - Metadata = new Dictionary - { - [organization.GatewayIdField()] = organization.Id.ToString() - }, - OffSession = true, - ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, - TrialPeriodDays = plan.TrialPeriodDays - }; - - var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); - - organization.GatewaySubscriptionId = subscription.Id; - - await organizationRepository.ReplaceAsync(organization); - - logger.LogInformation("CB: Recreated subscription for organization ({OrganizationID})", organization.Id); - } - else - { - logger.LogInformation( - "CB: Did not recreate subscription for organization ({OrganizationID}) as it already exists", - organization.Id); - } - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, - ClientMigrationProgress.RecreatedSubscription); - } - - private async Task ReverseOrganizationUpdateAsync(Guid providerId, Organization organization) - { - var migrationRecord = await clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id); - - if (migrationRecord == null) - { - logger.LogError( - "CB: Cannot reverse migration for organization ({OrganizationID}) as it does not have a migration record", - organization.Id); - - throw new Exception(); - } - - var plan = await pricingClient.GetPlanOrThrow(migrationRecord.PlanType); - - ResetOrganizationPlan(organization, plan); - organization.MaxStorageGb = migrationRecord.MaxStorageGb; - organization.ExpirationDate = migrationRecord.ExpirationDate; - organization.MaxAutoscaleSeats = migrationRecord.MaxAutoscaleSeats; - organization.Status = migrationRecord.Status; - - await organizationRepository.ReplaceAsync(organization); - - logger.LogInformation("CB: Reversed organization ({OrganizationID}) updates", - organization.Id); - - await migrationTrackerCache.UpdateTrackingStatus(providerId, organization.Id, - ClientMigrationProgress.ResetOrganization); - } - - #endregion - - #region Shared - - private static void ResetOrganizationPlan(Organization organization, Plan plan) - { - organization.Plan = plan.Name; - organization.PlanType = plan.Type; - organization.MaxCollections = plan.PasswordManager.MaxCollections; - organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; - organization.UsePolicies = plan.HasPolicies; - organization.UseSso = plan.HasSso; - organization.UseOrganizationDomains = plan.HasOrganizationDomains; - organization.UseGroups = plan.HasGroups; - organization.UseEvents = plan.HasEvents; - organization.UseDirectory = plan.HasDirectory; - organization.UseTotp = plan.HasTotp; - organization.Use2fa = plan.Has2fa; - organization.UseApi = plan.HasApi; - organization.UseResetPassword = plan.HasResetPassword; - organization.SelfHost = plan.HasSelfHost; - organization.UsersGetPremium = plan.UsersGetPremium; - organization.UseCustomPermissions = plan.HasCustomPermissions; - organization.UseScim = plan.HasScim; - organization.UseKeyConnector = plan.HasKeyConnector; - } - - #endregion -} diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs deleted file mode 100644 index e155b427f1..0000000000 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs +++ /dev/null @@ -1,436 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; -using Bit.Core.Billing.Providers.Entities; -using Bit.Core.Billing.Providers.Migration.Models; -using Bit.Core.Billing.Providers.Models; -using Bit.Core.Billing.Providers.Repositories; -using Bit.Core.Billing.Providers.Services; -using Bit.Core.Enums; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.Extensions.Logging; -using Stripe; - -namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; - -public class ProviderMigrator( - IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, - IOrganizationMigrator organizationMigrator, - ILogger logger, - IMigrationTrackerCache migrationTrackerCache, - IOrganizationRepository organizationRepository, - IPaymentService paymentService, - IProviderBillingService providerBillingService, - IProviderOrganizationRepository providerOrganizationRepository, - IProviderRepository providerRepository, - IProviderPlanRepository providerPlanRepository, - IStripeAdapter stripeAdapter) : IProviderMigrator -{ - public async Task Migrate(Guid providerId) - { - var provider = await GetProviderAsync(providerId); - - if (provider == null) - { - return; - } - - logger.LogInformation("CB: Starting migration for provider ({ProviderID})", providerId); - - await migrationTrackerCache.StartTracker(provider); - - var organizations = await GetClientsAsync(provider.Id); - - if (organizations.Count == 0) - { - logger.LogInformation("CB: Skipping migration for provider ({ProviderID}) with no clients", providerId); - - await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.NoClients); - - return; - } - - await MigrateClientsAsync(providerId, organizations); - - await ConfigureTeamsPlanAsync(providerId); - - await ConfigureEnterprisePlanAsync(providerId); - - await SetupCustomerAsync(provider); - - await SetupSubscriptionAsync(provider); - - await ApplyCreditAsync(provider); - - await UpdateProviderAsync(provider); - } - - public async Task GetResult(Guid providerId) - { - var providerTracker = await migrationTrackerCache.GetTracker(providerId); - - if (providerTracker == null) - { - return null; - } - - if (providerTracker.Progress == ProviderMigrationProgress.NoClients) - { - return new ProviderMigrationResult - { - ProviderId = providerTracker.ProviderId, - ProviderName = providerTracker.ProviderName, - Result = providerTracker.Progress.ToString() - }; - } - - var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId => - migrationTrackerCache.GetTracker(providerId, organizationId))); - - var migrationRecordLookup = new Dictionary(); - - foreach (var clientTracker in clientTrackers) - { - var migrationRecord = - await clientOrganizationMigrationRecordRepository.GetByOrganizationId(clientTracker.OrganizationId); - - migrationRecordLookup.Add(clientTracker.OrganizationId, migrationRecord); - } - - return new ProviderMigrationResult - { - ProviderId = providerTracker.ProviderId, - ProviderName = providerTracker.ProviderName, - Result = providerTracker.Progress.ToString(), - Clients = clientTrackers.Select(tracker => - { - var foundMigrationRecord = migrationRecordLookup.TryGetValue(tracker.OrganizationId, out var migrationRecord); - return new ClientMigrationResult - { - OrganizationId = tracker.OrganizationId, - OrganizationName = tracker.OrganizationName, - Result = tracker.Progress.ToString(), - PreviousState = foundMigrationRecord ? new ClientPreviousState(migrationRecord) : null - }; - }).ToList(), - }; - } - - #region Steps - - private async Task MigrateClientsAsync(Guid providerId, List organizations) - { - logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId); - - var organizationIds = organizations.Select(organization => organization.Id); - - await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds); - - foreach (var organization in organizations) - { - var tracker = await migrationTrackerCache.GetTracker(providerId, organization.Id); - - if (tracker is not { Progress: ClientMigrationProgress.Completed }) - { - await organizationMigrator.Migrate(providerId, organization); - } - } - - logger.LogInformation("CB: Migrated clients for provider ({ProviderID})", providerId); - - await migrationTrackerCache.UpdateTrackingStatus(providerId, - ProviderMigrationProgress.ClientsMigrated); - } - - private async Task ConfigureTeamsPlanAsync(Guid providerId) - { - logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId); - - var organizations = await GetClientsAsync(providerId); - - var teamsSeats = organizations - .Where(IsTeams) - .Sum(client => client.Seats) ?? 0; - - var teamsProviderPlan = (await providerPlanRepository.GetByProviderId(providerId)) - .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly); - - if (teamsProviderPlan == null) - { - await providerPlanRepository.CreateAsync(new ProviderPlan - { - ProviderId = providerId, - PlanType = PlanType.TeamsMonthly, - SeatMinimum = teamsSeats, - PurchasedSeats = 0, - AllocatedSeats = teamsSeats - }); - - logger.LogInformation("CB: Created Teams plan for provider ({ProviderID}) with a seat minimum of {Seats}", - providerId, teamsSeats); - } - else - { - logger.LogInformation("CB: Teams plan already exists for provider ({ProviderID}), updating seat minimum", providerId); - - teamsProviderPlan.SeatMinimum = teamsSeats; - teamsProviderPlan.AllocatedSeats = teamsSeats; - - await providerPlanRepository.ReplaceAsync(teamsProviderPlan); - - logger.LogInformation("CB: Updated Teams plan for provider ({ProviderID}) to seat minimum of {Seats}", - providerId, teamsProviderPlan.SeatMinimum); - } - - await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.TeamsPlanConfigured); - } - - private async Task ConfigureEnterprisePlanAsync(Guid providerId) - { - logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId); - - var organizations = await GetClientsAsync(providerId); - - var enterpriseSeats = organizations - .Where(IsEnterprise) - .Sum(client => client.Seats) ?? 0; - - var enterpriseProviderPlan = (await providerPlanRepository.GetByProviderId(providerId)) - .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly); - - if (enterpriseProviderPlan == null) - { - await providerPlanRepository.CreateAsync(new ProviderPlan - { - ProviderId = providerId, - PlanType = PlanType.EnterpriseMonthly, - SeatMinimum = enterpriseSeats, - PurchasedSeats = 0, - AllocatedSeats = enterpriseSeats - }); - - logger.LogInformation("CB: Created Enterprise plan for provider ({ProviderID}) with a seat minimum of {Seats}", - providerId, enterpriseSeats); - } - else - { - logger.LogInformation("CB: Enterprise plan already exists for provider ({ProviderID}), updating seat minimum", providerId); - - enterpriseProviderPlan.SeatMinimum = enterpriseSeats; - enterpriseProviderPlan.AllocatedSeats = enterpriseSeats; - - await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan); - - logger.LogInformation("CB: Updated Enterprise plan for provider ({ProviderID}) to seat minimum of {Seats}", - providerId, enterpriseProviderPlan.SeatMinimum); - } - - await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.EnterprisePlanConfigured); - } - - private async Task SetupCustomerAsync(Provider provider) - { - if (string.IsNullOrEmpty(provider.GatewayCustomerId)) - { - var organizations = await GetClientsAsync(provider.Id); - - var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId)); - - if (sampleOrganization == null) - { - logger.LogInformation( - "CB: Could not find sample organization for provider ({ProviderID}) that has a Stripe customer", - provider.Id); - - return; - } - - var taxInfo = await paymentService.GetTaxInfoAsync(sampleOrganization); - - // Create dummy payment source for legacy migration - this migrator is deprecated and will be removed - var dummyPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "migration_dummy_token"); - - var customer = await providerBillingService.SetupCustomer(provider, null, null); - - await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Coupon = StripeConstants.CouponIDs.LegacyMSPDiscount - }); - - provider.GatewayCustomerId = customer.Id; - - await providerRepository.ReplaceAsync(provider); - - logger.LogInformation("CB: Setup Stripe customer for provider ({ProviderID})", provider.Id); - } - else - { - logger.LogInformation("CB: Stripe customer already exists for provider ({ProviderID})", provider.Id); - } - - await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CustomerSetup); - } - - private async Task SetupSubscriptionAsync(Provider provider) - { - if (string.IsNullOrEmpty(provider.GatewaySubscriptionId)) - { - if (!string.IsNullOrEmpty(provider.GatewayCustomerId)) - { - var subscription = await providerBillingService.SetupSubscription(provider); - - provider.GatewaySubscriptionId = subscription.Id; - - await providerRepository.ReplaceAsync(provider); - - logger.LogInformation("CB: Setup Stripe subscription for provider ({ProviderID})", provider.Id); - } - else - { - logger.LogInformation( - "CB: Could not set up Stripe subscription for provider ({ProviderID}) with no Stripe customer", - provider.Id); - - return; - } - } - else - { - logger.LogInformation("CB: Stripe subscription already exists for provider ({ProviderID})", provider.Id); - - var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); - - var enterpriseSeatMinimum = providerPlans - .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly)? - .SeatMinimum ?? 0; - - var teamsSeatMinimum = providerPlans - .FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)? - .SeatMinimum ?? 0; - - var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand( - provider, - [ - (Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum), - (Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum) - ]); - await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand); - - logger.LogInformation( - "CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id); - } - - await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.SubscriptionSetup); - } - - private async Task ApplyCreditAsync(Provider provider) - { - var organizations = await GetClientsAsync(provider.Id); - - var organizationCustomers = - await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId))); - - var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance); - - if (organizationCancellationCredit != 0) - { - await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, - new CustomerBalanceTransactionCreateOptions - { - Amount = organizationCancellationCredit, - Currency = "USD", - Description = "Unused, prorated time for client organization subscriptions." - }); - } - - var migrationRecords = await Task.WhenAll(organizations.Select(organization => - clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id))); - - var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord => - migrationRecord.PlanType is - PlanType.EnterpriseAnnually2020 or - PlanType.TeamsAnnually2020); - - var legacyOrganizationCredit = legacyOrganizationMigrationRecords.Sum(migrationRecord => migrationRecord.Seats) * 12 * -100; - - if (legacyOrganizationCredit < 0) - { - await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId, - new CustomerBalanceTransactionCreateOptions - { - Amount = legacyOrganizationCredit, - Currency = "USD", - Description = "1 year rebate for legacy client organizations." - }); - } - - logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit + legacyOrganizationCredit, provider.Id); - - await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied); - } - - private async Task UpdateProviderAsync(Provider provider) - { - provider.Status = ProviderStatusType.Billable; - - await providerRepository.ReplaceAsync(provider); - - logger.LogInformation("CB: Completed migration for provider ({ProviderID})", provider.Id); - - await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.Completed); - } - - #endregion - - #region Utilities - - private async Task> GetClientsAsync(Guid providerId) - { - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); - - return (await Task.WhenAll(providerOrganizations.Select(providerOrganization => - organizationRepository.GetByIdAsync(providerOrganization.OrganizationId)))) - .ToList(); - } - - private async Task GetProviderAsync(Guid providerId) - { - var provider = await providerRepository.GetByIdAsync(providerId); - - if (provider == null) - { - logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it does not exist", providerId); - - return null; - } - - if (provider.Type != ProviderType.Msp) - { - logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it is not an MSP", providerId); - - return null; - } - - if (provider.Status == ProviderStatusType.Created) - { - return provider; - } - - logger.LogWarning("CB: Cannot migrate provider ({ProviderID}) as it is not in the 'Created' state", providerId); - - return null; - } - - private static bool IsEnterprise(Organization organization) => organization.Plan.Contains("Enterprise"); - private static bool IsTeams(Organization organization) => organization.Plan.Contains("Teams"); - - #endregion -} diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 9db18278b6..e7e67c0a11 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -3,6 +3,7 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Tax.Models; @@ -108,7 +109,7 @@ public class PremiumUserBillingService( when subscription.Status == StripeConstants.SubscriptionStatus.Active: { user.Premium = true; - user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); break; } } diff --git a/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs index 351c75ace0..ee60597601 100644 --- a/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs +++ b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Repositories; @@ -65,7 +66,7 @@ public class RestartSubscriptionCommand( { organization.GatewaySubscriptionId = subscription.Id; organization.Enabled = true; - organization.ExpirationDate = subscription.CurrentPeriodEnd; + organization.ExpirationDate = subscription.GetCurrentPeriodEnd(); organization.RevisionDate = DateTime.UtcNow; await organizationRepository.ReplaceAsync(organization); break; @@ -82,7 +83,7 @@ public class RestartSubscriptionCommand( { user.GatewaySubscriptionId = subscription.Id; user.Premium = true; - user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); user.RevisionDate = DateTime.UtcNow; await userRepository.ReplaceAsync(user); break; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 23cb885bd4..4c7d4ffc97 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -57,7 +57,7 @@ - + diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index a016ac54f3..f8a96a189f 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Billing.Extensions; using Stripe; namespace Bit.Core.Models.Business; @@ -36,8 +37,13 @@ public class SubscriptionInfo Status = sub.Status; TrialStartDate = sub.TrialStart; TrialEndDate = sub.TrialEnd; - PeriodStartDate = sub.CurrentPeriodStart; - PeriodEndDate = sub.CurrentPeriodEnd; + var currentPeriod = sub.GetCurrentPeriod(); + if (currentPeriod != null) + { + var (start, end) = currentPeriod.Value; + PeriodStartDate = start; + PeriodEndDate = end; + } CancelledDate = sub.CanceledAt; CancelAtEndDate = sub.CancelAtPeriodEnd; Cancelled = sub.Status == "canceled" || sub.Status == "unpaid" || sub.Status == "incomplete_expired"; diff --git a/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs b/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs deleted file mode 100644 index 34662ecdbb..0000000000 --- a/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs +++ /dev/null @@ -1,51 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -namespace Bit.Core.Models.BitStripe; - -// Stripe's SubscriptionListOptions model has a complex input for date filters. -// It expects a dictionary, and has lots of validation rules around what can have a value and what can't. -// To simplify this a bit we are extending Stripe's model and using our own date inputs, and building the dictionary they expect JiT. -// ___ -// Our model also facilitates selecting all elements in a list, which is unsupported by Stripe's model. -public class StripeSubscriptionListOptions : Stripe.SubscriptionListOptions -{ - public DateTime? CurrentPeriodEndDate { get; set; } - public string CurrentPeriodEndRange { get; set; } = "lt"; - public bool SelectAll { get; set; } - public new Stripe.DateRangeOptions CurrentPeriodEnd - { - get - { - return CurrentPeriodEndDate.HasValue ? - new Stripe.DateRangeOptions() - { - LessThan = CurrentPeriodEndRange == "lt" ? CurrentPeriodEndDate : null, - GreaterThan = CurrentPeriodEndRange == "gt" ? CurrentPeriodEndDate : null - } : - null; - } - } - - public Stripe.SubscriptionListOptions ToStripeApiOptions() - { - var stripeApiOptions = (Stripe.SubscriptionListOptions)this; - - if (SelectAll) - { - stripeApiOptions.EndingBefore = null; - stripeApiOptions.StartingAfter = null; - } - - if (CurrentPeriodEndDate.HasValue) - { - stripeApiOptions.CurrentPeriodEnd = new Stripe.DateRangeOptions() - { - LessThan = CurrentPeriodEndRange == "lt" ? CurrentPeriodEndDate : null, - GreaterThan = CurrentPeriodEndRange == "gt" ? CurrentPeriodEndDate : null - }; - } - - return stripeApiOptions; - } -} diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index 8a41263956..6b2c3c299e 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -3,58 +3,47 @@ using Bit.Core.Models.BitStripe; using Stripe; +using Stripe.Tax; namespace Bit.Core.Services; public interface IStripeAdapter { - Task CustomerCreateAsync(Stripe.CustomerCreateOptions customerCreateOptions); - Task CustomerGetAsync(string id, Stripe.CustomerGetOptions options = null); - Task CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null); - Task CustomerDeleteAsync(string id); - Task> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null); + Task CustomerCreateAsync(CustomerCreateOptions customerCreateOptions); + Task CustomerDeleteDiscountAsync(string customerId, CustomerDeleteDiscountOptions options = null); + Task CustomerGetAsync(string id, CustomerGetOptions options = null); + Task CustomerUpdateAsync(string id, CustomerUpdateOptions options = null); + Task CustomerDeleteAsync(string id); + Task> CustomerListPaymentMethods(string id, CustomerPaymentMethodListOptions options = null); Task CustomerBalanceTransactionCreate(string customerId, CustomerBalanceTransactionCreateOptions options); - Task SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions); - Task SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null); - - /// - /// Retrieves a subscription object for a provider. - /// - /// The subscription ID. - /// The provider ID. - /// Additional options. - /// The subscription object. - /// Thrown when the subscription doesn't belong to the provider. - Task ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null); - - Task> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions); - Task SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null); - Task SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null); - Task InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options); - Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options); - Task> InvoiceListAsync(StripeInvoiceListOptions options); - Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options); - Task> InvoiceSearchAsync(InvoiceSearchOptions options); - Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options); - Task InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options); - Task InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options); - Task InvoicePayAsync(string id, Stripe.InvoicePayOptions options = null); - Task InvoiceDeleteAsync(string id, Stripe.InvoiceDeleteOptions options = null); - Task InvoiceVoidInvoiceAsync(string id, Stripe.InvoiceVoidOptions options = null); - IEnumerable PaymentMethodListAutoPaging(Stripe.PaymentMethodListOptions options); - IAsyncEnumerable PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options); - Task PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null); - Task PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null); - Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options); - Task TaxIdDeleteAsync(string customerId, string taxIdId, Stripe.TaxIdDeleteOptions options = null); - Task> TaxRegistrationsListAsync(Stripe.Tax.RegistrationListOptions options = null); - Task> ChargeListAsync(Stripe.ChargeListOptions options); - Task RefundCreateAsync(Stripe.RefundCreateOptions options); - Task CardDeleteAsync(string customerId, string cardId, Stripe.CardDeleteOptions options = null); - Task BankAccountCreateAsync(string customerId, Stripe.BankAccountCreateOptions options = null); - Task BankAccountDeleteAsync(string customerId, string bankAccount, Stripe.BankAccountDeleteOptions options = null); - Task> PriceListAsync(Stripe.PriceListOptions options = null); + Task SubscriptionCreateAsync(SubscriptionCreateOptions subscriptionCreateOptions); + Task SubscriptionGetAsync(string id, SubscriptionGetOptions options = null); + Task SubscriptionUpdateAsync(string id, SubscriptionUpdateOptions options = null); + Task SubscriptionCancelAsync(string Id, SubscriptionCancelOptions options = null); + Task InvoiceGetAsync(string id, InvoiceGetOptions options); + Task> InvoiceListAsync(StripeInvoiceListOptions options); + Task InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options); + Task> InvoiceSearchAsync(InvoiceSearchOptions options); + Task InvoiceUpdateAsync(string id, InvoiceUpdateOptions options); + Task InvoiceFinalizeInvoiceAsync(string id, InvoiceFinalizeOptions options); + Task InvoiceSendInvoiceAsync(string id, InvoiceSendOptions options); + Task InvoicePayAsync(string id, InvoicePayOptions options = null); + Task InvoiceDeleteAsync(string id, InvoiceDeleteOptions options = null); + Task InvoiceVoidInvoiceAsync(string id, InvoiceVoidOptions options = null); + IEnumerable PaymentMethodListAutoPaging(PaymentMethodListOptions options); + IAsyncEnumerable PaymentMethodListAutoPagingAsync(PaymentMethodListOptions options); + Task PaymentMethodAttachAsync(string id, PaymentMethodAttachOptions options = null); + Task PaymentMethodDetachAsync(string id, PaymentMethodDetachOptions options = null); + Task TaxIdCreateAsync(string id, TaxIdCreateOptions options); + Task TaxIdDeleteAsync(string customerId, string taxIdId, TaxIdDeleteOptions options = null); + Task> TaxRegistrationsListAsync(RegistrationListOptions options = null); + Task> ChargeListAsync(ChargeListOptions options); + Task RefundCreateAsync(RefundCreateOptions options); + Task CardDeleteAsync(string customerId, string cardId, CardDeleteOptions options = null); + Task BankAccountCreateAsync(string customerId, BankAccountCreateOptions options = null); + Task BankAccountDeleteAsync(string customerId, string bankAccount, BankAccountDeleteOptions options = null); + Task> PriceListAsync(PriceListOptions options = null); Task SetupIntentCreate(SetupIntentCreateOptions options); Task> SetupIntentList(SetupIntentListOptions options); Task SetupIntentCancel(string id, SetupIntentCancelOptions options = null); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 4863baf73e..3d1663f021 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -9,18 +9,18 @@ namespace Bit.Core.Services; public class StripeAdapter : IStripeAdapter { - private readonly Stripe.CustomerService _customerService; - private readonly Stripe.SubscriptionService _subscriptionService; - private readonly Stripe.InvoiceService _invoiceService; - private readonly Stripe.PaymentMethodService _paymentMethodService; - private readonly Stripe.TaxIdService _taxIdService; - private readonly Stripe.ChargeService _chargeService; - private readonly Stripe.RefundService _refundService; - private readonly Stripe.CardService _cardService; - private readonly Stripe.BankAccountService _bankAccountService; - private readonly Stripe.PlanService _planService; - private readonly Stripe.PriceService _priceService; - private readonly Stripe.SetupIntentService _setupIntentService; + private readonly CustomerService _customerService; + private readonly SubscriptionService _subscriptionService; + private readonly InvoiceService _invoiceService; + private readonly PaymentMethodService _paymentMethodService; + private readonly TaxIdService _taxIdService; + private readonly ChargeService _chargeService; + private readonly RefundService _refundService; + private readonly CardService _cardService; + private readonly BankAccountService _bankAccountService; + private readonly PlanService _planService; + private readonly PriceService _priceService; + private readonly SetupIntentService _setupIntentService; private readonly Stripe.TestHelpers.TestClockService _testClockService; private readonly CustomerBalanceTransactionService _customerBalanceTransactionService; private readonly Stripe.Tax.RegistrationService _taxRegistrationService; @@ -28,17 +28,17 @@ public class StripeAdapter : IStripeAdapter public StripeAdapter() { - _customerService = new Stripe.CustomerService(); - _subscriptionService = new Stripe.SubscriptionService(); - _invoiceService = new Stripe.InvoiceService(); - _paymentMethodService = new Stripe.PaymentMethodService(); - _taxIdService = new Stripe.TaxIdService(); - _chargeService = new Stripe.ChargeService(); - _refundService = new Stripe.RefundService(); - _cardService = new Stripe.CardService(); - _bankAccountService = new Stripe.BankAccountService(); - _priceService = new Stripe.PriceService(); - _planService = new Stripe.PlanService(); + _customerService = new CustomerService(); + _subscriptionService = new SubscriptionService(); + _invoiceService = new InvoiceService(); + _paymentMethodService = new PaymentMethodService(); + _taxIdService = new TaxIdService(); + _chargeService = new ChargeService(); + _refundService = new RefundService(); + _cardService = new CardService(); + _bankAccountService = new BankAccountService(); + _priceService = new PriceService(); + _planService = new PlanService(); _setupIntentService = new SetupIntentService(); _testClockService = new Stripe.TestHelpers.TestClockService(); _customerBalanceTransactionService = new CustomerBalanceTransactionService(); @@ -46,28 +46,31 @@ public class StripeAdapter : IStripeAdapter _calculationService = new CalculationService(); } - public Task CustomerCreateAsync(Stripe.CustomerCreateOptions options) + public Task CustomerCreateAsync(CustomerCreateOptions options) { return _customerService.CreateAsync(options); } - public Task CustomerGetAsync(string id, Stripe.CustomerGetOptions options = null) + public Task CustomerDeleteDiscountAsync(string customerId, CustomerDeleteDiscountOptions options = null) => + _customerService.DeleteDiscountAsync(customerId, options); + + public Task CustomerGetAsync(string id, CustomerGetOptions options = null) { return _customerService.GetAsync(id, options); } - public Task CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null) + public Task CustomerUpdateAsync(string id, CustomerUpdateOptions options = null) { return _customerService.UpdateAsync(id, options); } - public Task CustomerDeleteAsync(string id) + public Task CustomerDeleteAsync(string id) { return _customerService.DeleteAsync(id); } public async Task> CustomerListPaymentMethods(string id, - CustomerListPaymentMethodsOptions options = null) + CustomerPaymentMethodListOptions options = null) { var paymentMethods = await _customerService.ListPaymentMethodsAsync(id, options); return paymentMethods.Data; @@ -77,12 +80,12 @@ public class StripeAdapter : IStripeAdapter CustomerBalanceTransactionCreateOptions options) => await _customerBalanceTransactionService.CreateAsync(customerId, options); - public Task SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options) + public Task SubscriptionCreateAsync(SubscriptionCreateOptions options) { return _subscriptionService.CreateAsync(options); } - public Task SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null) + public Task SubscriptionGetAsync(string id, SubscriptionGetOptions options = null) { return _subscriptionService.GetAsync(id, options); } @@ -101,28 +104,23 @@ public class StripeAdapter : IStripeAdapter throw new InvalidOperationException("Subscription does not belong to the provider."); } - public Task SubscriptionUpdateAsync(string id, - Stripe.SubscriptionUpdateOptions options = null) + public Task SubscriptionUpdateAsync(string id, + SubscriptionUpdateOptions options = null) { return _subscriptionService.UpdateAsync(id, options); } - public Task SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null) + public Task SubscriptionCancelAsync(string Id, SubscriptionCancelOptions options = null) { return _subscriptionService.CancelAsync(Id, options); } - public Task InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options) - { - return _invoiceService.UpcomingAsync(options); - } - - public Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options) + public Task InvoiceGetAsync(string id, InvoiceGetOptions options) { return _invoiceService.GetAsync(id, options); } - public async Task> InvoiceListAsync(StripeInvoiceListOptions options) + public async Task> InvoiceListAsync(StripeInvoiceListOptions options) { if (!options.SelectAll) { @@ -131,7 +129,7 @@ public class StripeAdapter : IStripeAdapter options.Limit = 100; - var invoices = new List(); + var invoices = new List(); await foreach (var invoice in _invoiceService.ListAutoPagingAsync(options.ToInvoiceListOptions())) { @@ -146,120 +144,104 @@ public class StripeAdapter : IStripeAdapter return _invoiceService.CreatePreviewAsync(options); } - public async Task> InvoiceSearchAsync(InvoiceSearchOptions options) + public async Task> InvoiceSearchAsync(InvoiceSearchOptions options) => (await _invoiceService.SearchAsync(options)).Data; - public Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options) + public Task InvoiceUpdateAsync(string id, InvoiceUpdateOptions options) { return _invoiceService.UpdateAsync(id, options); } - public Task InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options) + public Task InvoiceFinalizeInvoiceAsync(string id, InvoiceFinalizeOptions options) { return _invoiceService.FinalizeInvoiceAsync(id, options); } - public Task InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options) + public Task InvoiceSendInvoiceAsync(string id, InvoiceSendOptions options) { return _invoiceService.SendInvoiceAsync(id, options); } - public Task InvoicePayAsync(string id, Stripe.InvoicePayOptions options = null) + public Task InvoicePayAsync(string id, InvoicePayOptions options = null) { return _invoiceService.PayAsync(id, options); } - public Task InvoiceDeleteAsync(string id, Stripe.InvoiceDeleteOptions options = null) + public Task InvoiceDeleteAsync(string id, InvoiceDeleteOptions options = null) { return _invoiceService.DeleteAsync(id, options); } - public Task InvoiceVoidInvoiceAsync(string id, Stripe.InvoiceVoidOptions options = null) + public Task InvoiceVoidInvoiceAsync(string id, InvoiceVoidOptions options = null) { return _invoiceService.VoidInvoiceAsync(id, options); } - public IEnumerable PaymentMethodListAutoPaging(Stripe.PaymentMethodListOptions options) + public IEnumerable PaymentMethodListAutoPaging(PaymentMethodListOptions options) { return _paymentMethodService.ListAutoPaging(options); } - public IAsyncEnumerable PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options) + public IAsyncEnumerable PaymentMethodListAutoPagingAsync(PaymentMethodListOptions options) => _paymentMethodService.ListAutoPagingAsync(options); - public Task PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null) + public Task PaymentMethodAttachAsync(string id, PaymentMethodAttachOptions options = null) { return _paymentMethodService.AttachAsync(id, options); } - public Task PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null) + public Task PaymentMethodDetachAsync(string id, PaymentMethodDetachOptions options = null) { return _paymentMethodService.DetachAsync(id, options); } - public Task PlanGetAsync(string id, Stripe.PlanGetOptions options = null) + public Task PlanGetAsync(string id, PlanGetOptions options = null) { return _planService.GetAsync(id, options); } - public Task TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options) + public Task TaxIdCreateAsync(string id, TaxIdCreateOptions options) { return _taxIdService.CreateAsync(id, options); } - public Task TaxIdDeleteAsync(string customerId, string taxIdId, - Stripe.TaxIdDeleteOptions options = null) + public Task TaxIdDeleteAsync(string customerId, string taxIdId, + TaxIdDeleteOptions options = null) { return _taxIdService.DeleteAsync(customerId, taxIdId); } - public Task> TaxRegistrationsListAsync(Stripe.Tax.RegistrationListOptions options = null) + public Task> TaxRegistrationsListAsync(RegistrationListOptions options = null) { return _taxRegistrationService.ListAsync(options); } - public Task> ChargeListAsync(Stripe.ChargeListOptions options) + public Task> ChargeListAsync(ChargeListOptions options) { return _chargeService.ListAsync(options); } - public Task RefundCreateAsync(Stripe.RefundCreateOptions options) + public Task RefundCreateAsync(RefundCreateOptions options) { return _refundService.CreateAsync(options); } - public Task CardDeleteAsync(string customerId, string cardId, Stripe.CardDeleteOptions options = null) + public Task CardDeleteAsync(string customerId, string cardId, CardDeleteOptions options = null) { return _cardService.DeleteAsync(customerId, cardId, options); } - public Task BankAccountCreateAsync(string customerId, Stripe.BankAccountCreateOptions options = null) + public Task BankAccountCreateAsync(string customerId, BankAccountCreateOptions options = null) { return _bankAccountService.CreateAsync(customerId, options); } - public Task BankAccountDeleteAsync(string customerId, string bankAccount, Stripe.BankAccountDeleteOptions options = null) + public Task BankAccountDeleteAsync(string customerId, string bankAccount, BankAccountDeleteOptions options = null) { return _bankAccountService.DeleteAsync(customerId, bankAccount, options); } - public async Task> SubscriptionListAsync(StripeSubscriptionListOptions options) - { - if (!options.SelectAll) - { - return (await _subscriptionService.ListAsync(options.ToStripeApiOptions())).Data; - } - - options.Limit = 100; - var items = new List(); - await foreach (var i in _subscriptionService.ListAutoPagingAsync(options.ToStripeApiOptions())) - { - items.Add(i); - } - return items; - } - - public async Task> PriceListAsync(Stripe.PriceListOptions options = null) + public async Task> PriceListAsync(PriceListOptions options = null) { return await _priceService.ListAsync(options); } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 5b68906d8a..bb53933d02 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -65,19 +65,20 @@ public class StripePaymentService : IPaymentService bool applySponsorship) { var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType); - var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ? - Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : - null; - var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); + var sponsoredPlan = sponsorship?.PlanSponsorshipType != null + ? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) + : null; + var subscriptionUpdate = + new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, true); var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); - org.ExpirationDate = sub.CurrentPeriodEnd; + org.ExpirationDate = sub.GetCurrentPeriodEnd(); if (sponsorship is not null) { - sponsorship.ValidUntil = sub.CurrentPeriodEnd; + sponsorship.ValidUntil = sub.GetCurrentPeriodEnd(); } } @@ -100,7 +101,8 @@ public class StripePaymentService : IPaymentService if (sub.Status == SubscriptionStatuses.Canceled) { - throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes."); + throw new BadRequestException( + "You do not have an active subscription. Reinstate your subscription to make changes."); } var existingCoupon = sub.Customer.Discount?.Coupon?.Id; @@ -191,24 +193,24 @@ public class StripePaymentService : IPaymentService throw; } } - else if (!invoice.Paid) + else if (invoice.Status != StripeConstants.InvoiceStatus.Paid) { // Pay invoice with no charge to the customer this completes the invoice immediately without waiting the scheduled 1h invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId); paymentIntentClientSecret = null; } - } finally { // Change back the subscription collection method and/or days until due if (collectionMethod != "send_invoice" || daysUntilDue == null) { - await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions - { - CollectionMethod = collectionMethod, - DaysUntilDue = daysUntilDue, - }); + await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, + new SubscriptionUpdateOptions + { + CollectionMethod = collectionMethod, + DaysUntilDue = daysUntilDue, + }); } var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId); @@ -218,9 +220,15 @@ public class StripePaymentService : IPaymentService if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon)) { // Re-add the lost coupon due to the update. - await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, new CustomerUpdateOptions + await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions { - Coupon = existingCoupon + Discounts = + [ + new SubscriptionDiscountOptions + { + Coupon = existingCoupon + } + ] }); } } @@ -352,7 +360,7 @@ public class StripePaymentService : IPaymentService { var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card"; var hasDefaultValidSource = customer.DefaultSource != null && - (customer.DefaultSource is Card || customer.DefaultSource is BankAccount); + (customer.DefaultSource is Card || customer.DefaultSource is BankAccount); if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource) { cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id; @@ -365,12 +373,11 @@ public class StripePaymentService : IPaymentService } catch { - await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions - { - AutoAdvance = false - }); + await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, + new InvoiceFinalizeOptions { AutoAdvance = false }); await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id); } + throw new BadRequestException("No payment method is available."); } } @@ -381,14 +388,9 @@ public class StripePaymentService : IPaymentService { // Finalize the invoice (from Draft) w/o auto-advance so we // can attempt payment manually. - invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions - { - AutoAdvance = false, - }); - var invoicePayOptions = new InvoicePayOptions - { - PaymentMethod = cardPaymentMethodId, - }; + invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, + new InvoiceFinalizeOptions { AutoAdvance = false, }); + var invoicePayOptions = new InvoicePayOptions { PaymentMethod = cardPaymentMethodId, }; if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false) { invoicePayOptions.PaidOutOfBand = true; @@ -403,13 +405,15 @@ public class StripePaymentService : IPaymentService SubmitForSettlement = true, PayPal = new Braintree.TransactionOptionsPayPalRequest { - CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}" + CustomField = + $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}" } }, CustomFields = new Dictionary { [subscriber.BraintreeIdField()] = subscriber.Id.ToString(), - [subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion + [subscriber.BraintreeCloudRegionField()] = + _globalSettings.BaseServiceUri.CloudRegion } }); @@ -442,9 +446,9 @@ public class StripePaymentService : IPaymentService { // SCA required, get intent client secret var invoiceGetOptions = new InvoiceGetOptions(); - invoiceGetOptions.AddExpand("payment_intent"); + invoiceGetOptions.AddExpand("confirmation_secret"); invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions); - paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret; + paymentIntentClientSecret = invoice?.ConfirmationSecret?.ClientSecret; } else { @@ -458,6 +462,7 @@ public class StripePaymentService : IPaymentService { await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id); } + if (invoice != null) { if (invoice.Status == "paid") @@ -479,10 +484,8 @@ public class StripePaymentService : IPaymentService // Assumption: Customer balance should now be $0, otherwise payment would not have failed. if (customer.Balance == 0) { - await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Balance = invoice.StartingBalance - }); + await _stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { Balance = invoice.StartingBalance }); } } } @@ -496,6 +499,7 @@ public class StripePaymentService : IPaymentService // Let the caller perform any subscription change cleanup throw; } + return paymentIntentClientSecret; } @@ -526,10 +530,10 @@ public class StripePaymentService : IPaymentService try { - var canceledSub = endOfPeriod ? - await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, - new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) : - await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions()); + var canceledSub = endOfPeriod + ? await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, + new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) + : await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions()); if (!canceledSub.CanceledAt.HasValue) { throw new GatewayException("Unable to cancel subscription."); @@ -580,7 +584,7 @@ public class StripePaymentService : IPaymentService { Customer customer = null; var customerExists = subscriber.Gateway == GatewayType.Stripe && - !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId); + !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId); if (customerExists) { customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); @@ -595,10 +599,10 @@ public class StripePaymentService : IPaymentService subscriber.Gateway = GatewayType.Stripe; subscriber.GatewayCustomerId = customer.Id; } - await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Balance = customer.Balance - (long)(creditAmount * 100) - }); + + await _stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { Balance = customer.Balance - (long)(creditAmount * 100) }); + return !customerExists; } @@ -630,50 +634,45 @@ public class StripePaymentService : IPaymentService { var subscriptionInfo = new SubscriptionInfo(); - if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) - { - var customerGetOptions = new CustomerGetOptions(); - customerGetOptions.AddExpand("discount.coupon.applies_to"); - var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerGetOptions); - - if (customer.Discount != null) - { - subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(customer.Discount); - } - } - - if (string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) + if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) { return subscriptionInfo; } - var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, new SubscriptionGetOptions + var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, + new SubscriptionGetOptions { Expand = ["customer", "discounts", "test_clock"] }); + + subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(subscription); + + var discount = subscription.Customer.Discount ?? subscription.Discounts.FirstOrDefault(); + + if (discount != null) { - Expand = ["test_clock"] - }); - - if (sub != null) - { - subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); - - var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(sub); - - if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue) - { - subscriptionInfo.Subscription.SuspensionDate = suspensionDate; - subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate; - } + subscriptionInfo.CustomerDiscount = new SubscriptionInfo.BillingCustomerDiscount(discount); } - if (sub is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) + var (suspensionDate, unpaidPeriodEndDate) = await GetSuspensionDateAsync(subscription); + + if (suspensionDate.HasValue && unpaidPeriodEndDate.HasValue) + { + subscriptionInfo.Subscription.SuspensionDate = suspensionDate; + subscriptionInfo.Subscription.UnpaidPeriodEndDate = unpaidPeriodEndDate; + } + + if (subscription is { CanceledAt: not null } || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) { return subscriptionInfo; } try { - var upcomingInvoiceOptions = new UpcomingInvoiceOptions { Customer = subscriber.GatewayCustomerId }; - var upcomingInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions); + var invoiceCreatePreviewOptions = new InvoiceCreatePreviewOptions + { + Customer = subscriber.GatewayCustomerId, + Subscription = subscriber.GatewaySubscriptionId + }; + + var upcomingInvoice = await _stripeAdapter.InvoiceCreatePreviewAsync(invoiceCreatePreviewOptions); if (upcomingInvoice != null) { @@ -682,7 +681,12 @@ public class StripePaymentService : IPaymentService } catch (StripeException ex) { - _logger.LogWarning(ex, "Encountered an unexpected Stripe error"); + _logger.LogWarning( + ex, + "Failed to retrieve upcoming invoice for customer {CustomerId}, subscription {SubscriptionId}. Error Code: {ErrorCode}", + subscriber.GatewayCustomerId, + subscriber.GatewaySubscriptionId, + ex.StripeError?.Code); } return subscriptionInfo; @@ -788,7 +792,11 @@ public class StripePaymentService : IPaymentService if (taxInfo.TaxIdType == StripeConstants.TaxIdType.SpanishNIF) { await _stripeAdapter.TaxIdCreateAsync(customer.Id, - new TaxIdCreateOptions { Type = StripeConstants.TaxIdType.EUVAT, Value = $"ES{taxInfo.TaxIdNumber}" }); + new TaxIdCreateOptions + { + Type = StripeConstants.TaxIdType.EUVAT, + Value = $"ES{taxInfo.TaxIdNumber}" + }); } } catch (StripeException e) @@ -829,7 +837,8 @@ public class StripePaymentService : IPaymentService await HasSecretsManagerStandaloneAsync(gatewayCustomerId: organization.GatewayCustomerId, organizationHasSecretsManager: organization.UseSecretsManager); - private async Task HasSecretsManagerStandaloneAsync(string gatewayCustomerId, bool organizationHasSecretsManager) + private async Task HasSecretsManagerStandaloneAsync(string gatewayCustomerId, + bool organizationHasSecretsManager) { if (string.IsNullOrEmpty(gatewayCustomerId)) { @@ -894,26 +903,14 @@ public class StripePaymentService : IPaymentService { var options = new InvoiceCreatePreviewOptions { - AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = true, - }, + AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, }, Currency = "usd", SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = [ - new() - { - Quantity = 1, - Plan = StripeConstants.Prices.PremiumAnnually - }, - - new() - { - Quantity = parameters.PasswordManager.AdditionalStorage, - Plan = "storage-gb-annually" - } + new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually }, + new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal } ] }, CustomerDetails = new InvoiceCustomerDetailsOptions @@ -940,12 +937,9 @@ public class StripePaymentService : IPaymentService throw new BadRequestException("billingPreviewInvalidTaxIdError"); } - options.CustomerDetails.TaxIds = [ - new InvoiceCustomerDetailsTaxIdOptions - { - Type = taxIdType, - Value = parameters.TaxInformation.TaxId - } + options.CustomerDetails.TaxIds = + [ + new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId } ]; if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) @@ -964,7 +958,7 @@ public class StripePaymentService : IPaymentService if (gatewayCustomer.Discount != null) { - options.Coupon = gatewayCustomer.Discount.Coupon.Id; + options.Discounts = [new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id }]; } } @@ -972,24 +966,31 @@ public class StripePaymentService : IPaymentService { var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - if (gatewaySubscription?.Discount != null) + if (gatewaySubscription?.Discounts is { Count: > 0 }) { - options.Coupon ??= gatewaySubscription.Discount.Coupon.Id; + options.Discounts = gatewaySubscription.Discounts.Select(x => new InvoiceDiscountOptions { Coupon = x.Coupon.Id }).ToList(); } } + if (options.Discounts is { Count: > 0 }) + { + options.Discounts = options.Discounts.DistinctBy(invoiceDiscountOptions => invoiceDiscountOptions.Coupon).ToList(); + } + try { var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 - ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() + var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount); + + var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 + ? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() : 0M; var result = new PreviewInvoiceResponseModel( effectiveTaxRate, invoice.TotalExcludingTax.ToMajor() ?? 0, - invoice.Tax.ToMajor() ?? 0, + tax.ToMajor(), invoice.Total.ToMajor()); return result; } @@ -1003,7 +1004,8 @@ public class StripePaymentService : IPaymentService parameters.TaxInformation.Country); throw new BadRequestException("billingPreviewInvalidTaxIdError"); default: - _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", + _logger.LogError(e, + "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", parameters.TaxInformation.TaxId, parameters.TaxInformation.Country); throw new BadRequestException("billingPreviewInvoiceError"); @@ -1101,12 +1103,9 @@ public class StripePaymentService : IPaymentService throw new BadRequestException("billingTaxIdTypeInferenceError"); } - options.CustomerDetails.TaxIds = [ - new InvoiceCustomerDetailsTaxIdOptions - { - Type = taxIdType, - Value = parameters.TaxInformation.TaxId - } + options.CustomerDetails.TaxIds = + [ + new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId } ]; if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) @@ -1127,7 +1126,10 @@ public class StripePaymentService : IPaymentService if (gatewayCustomer.Discount != null) { - options.Coupon = gatewayCustomer.Discount.Coupon.Id; + options.Discounts = + [ + new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id } + ]; } } @@ -1135,9 +1137,10 @@ public class StripePaymentService : IPaymentService { var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - if (gatewaySubscription?.Discount != null) + if (gatewaySubscription?.Discounts != null) { - options.Coupon ??= gatewaySubscription.Discount.Coupon.Id; + options.Discounts = gatewaySubscription.Discounts + .Select(discount => new InvoiceDiscountOptions { Coupon = discount.Coupon.Id }).ToList(); } } @@ -1152,14 +1155,16 @@ public class StripePaymentService : IPaymentService { var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 - ? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() + var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount); + + var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 + ? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() : 0M; var result = new PreviewInvoiceResponseModel( effectiveTaxRate, invoice.TotalExcludingTax.ToMajor() ?? 0, - invoice.Tax.ToMajor() ?? 0, + tax.ToMajor(), invoice.Total.ToMajor()); return result; } @@ -1173,7 +1178,8 @@ public class StripePaymentService : IPaymentService parameters.TaxInformation.Country); throw new BadRequestException("billingPreviewInvalidTaxIdError"); default: - _logger.LogError(e, "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", + _logger.LogError(e, + "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", parameters.TaxInformation.TaxId, parameters.TaxInformation.Country); throw new BadRequestException("billingPreviewInvoiceError"); @@ -1207,7 +1213,9 @@ public class StripePaymentService : IPaymentService braintreeCustomer.DefaultPaymentMethod); } } - catch (Braintree.Exceptions.NotFoundException) { } + catch (Braintree.Exceptions.NotFoundException) + { + } } if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card") @@ -1246,12 +1254,15 @@ public class StripePaymentService : IPaymentService { customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options); } - catch (StripeException) { } + catch (StripeException) + { + } return customer; } - private async Task> GetBillingTransactionsAsync(ISubscriber subscriber, int? limit = null) + private async Task> GetBillingTransactionsAsync( + ISubscriber subscriber, int? limit = null) { var transactions = subscriber switch { diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 1a7530321e..0622c5cbb2 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -17,7 +17,4 @@ 71502
- - - diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 8c1dd60fb9..75bd13eae8 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; @@ -270,7 +271,6 @@ public class ProviderBillingControllerTests var subscription = new Subscription { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, - CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth), Customer = new Customer { Address = new Address @@ -291,20 +291,23 @@ public class ProviderBillingControllerTests Data = [ new SubscriptionItem { + CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth), Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise } }, new SubscriptionItem { + CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth), Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } } ] }, - Status = "unpaid", + Status = "unpaid" }; stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is( options => options.Expand.Contains("customer.tax_ids") && + options.Expand.Contains("discounts") && options.Expand.Contains("test_clock"))).Returns(subscription); var daysInLastMonth = DateTime.DaysInMonth(oneMonthAgo.Year, oneMonthAgo.Month); @@ -365,7 +368,7 @@ public class ProviderBillingControllerTests var response = ((Ok)result).Value; Assert.Equal(subscription.Status, response.Status); - Assert.Equal(subscription.CurrentPeriodEnd, response.CurrentPeriodEndDate); + Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate); Assert.Equal(subscription.Customer!.Discount!.Coupon!.PercentOff, response.DiscountPercentage); Assert.Equal(subscription.CollectionMethod, response.CollectionMethod); @@ -405,6 +408,118 @@ public class ProviderBillingControllerTests Assert.Equal(14, response.Suspension.GracePeriod); } + [Theory, BitAutoData] + public async Task GetSubscriptionAsync_SubscriptionLevelDiscount_Ok( + Provider provider, + SutProvider sutProvider) + { + ConfigureStableProviderServiceUserInputs(provider, sutProvider); + + var stripeAdapter = sutProvider.GetDependency(); + + var now = DateTime.UtcNow; + var oneMonthAgo = now.AddMonths(-1); + + var daysInThisMonth = DateTime.DaysInMonth(now.Year, now.Month); + + var subscription = new Subscription + { + CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, + Customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Example St.", + Line2 = "Unit 1", + City = "Example Town", + State = "NY" + }, + Balance = -100000, + Discount = null, // No customer-level discount + TaxIds = new StripeList { Data = [new TaxId { Value = "123456789" }] } + }, + Discounts = + [ + new Discount { Coupon = new Coupon { PercentOff = 15 } } // Subscription-level discount + ], + Items = new StripeList + { + Data = [ + new SubscriptionItem + { + CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth), + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise } + }, + new SubscriptionItem + { + CurrentPeriodEnd = new DateTime(now.Year, now.Month, daysInThisMonth), + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } + } + ] + }, + Status = "active" + }; + + stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId, Arg.Is( + options => + options.Expand.Contains("customer.tax_ids") && + options.Expand.Contains("discounts") && + options.Expand.Contains("test_clock"))).Returns(subscription); + + stripeAdapter.InvoiceSearchAsync(Arg.Is( + options => options.Query == $"subscription:'{subscription.Id}' status:'open'")) + .Returns([]); + + var providerPlans = new List + { + new () + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 50, + PurchasedSeats = 10, + AllocatedSeats = 60 + }, + new () + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 90 + } + }; + + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); + + foreach (var providerPlan in providerPlans) + { + var plan = StaticStore.GetPlan(providerPlan.PlanType); + sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); + sutProvider.GetDependency().PriceGetAsync(priceId) + .Returns(new Price + { + UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100 + }); + } + + var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); + + Assert.IsType>(result); + + var response = ((Ok)result).Value; + + Assert.Equal(subscription.Status, response.Status); + Assert.Equal(subscription.GetCurrentPeriodEnd(), response.CurrentPeriodEndDate); + Assert.Equal(15, response.DiscountPercentage); // Verify subscription-level discount is used + Assert.Equal(subscription.CollectionMethod, response.CollectionMethod); + } + #endregion #region UpdateTaxInformationAsync diff --git a/test/Billing.Test/Billing.Test.csproj b/test/Billing.Test/Billing.Test.csproj index b4ea2938f6..4d7f887c90 100644 --- a/test/Billing.Test/Billing.Test.csproj +++ b/test/Billing.Test/Billing.Test.csproj @@ -27,24 +27,6 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - PreserveNewest @@ -73,9 +55,6 @@ PreserveNewest - - PreserveNewest - diff --git a/test/Billing.Test/Resources/Events/charge.succeeded.json b/test/Billing.Test/Resources/Events/charge.succeeded.json deleted file mode 100644 index 3cf919f123..0000000000 --- a/test/Billing.Test/Resources/Events/charge.succeeded.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "id": "evt_3NvKgBIGBnsLynRr0pJJqudS", - "object": "event", - "api_version": "2024-06-20", - "created": 1695909300, - "data": { - "object": { - "id": "ch_3NvKgBIGBnsLynRr0ZyvP9AN", - "object": "charge", - "amount": 7200, - "amount_captured": 7200, - "amount_refunded": 0, - "application": null, - "application_fee": null, - "application_fee_amount": null, - "balance_transaction": "txn_3NvKgBIGBnsLynRr0KbYEz76", - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": null, - "name": null, - "phone": null - }, - "calculated_statement_descriptor": "BITWARDEN", - "captured": true, - "created": 1695909299, - "currency": "usd", - "customer": "cus_OimAwOzQmThNXx", - "description": "Subscription update", - "destination": null, - "dispute": null, - "disputed": false, - "failure_balance_transaction": null, - "failure_code": null, - "failure_message": null, - "fraud_details": { - }, - "invoice": "in_1NvKgBIGBnsLynRrmRFHAcoV", - "livemode": false, - "metadata": { - }, - "on_behalf_of": null, - "order": null, - "outcome": { - "network_status": "approved_by_network", - "reason": null, - "risk_level": "normal", - "risk_score": 37, - "seller_message": "Payment complete.", - "type": "authorized" - }, - "paid": true, - "payment_intent": "pi_3NvKgBIGBnsLynRr09Ny3Heu", - "payment_method": "pm_1NvKbpIGBnsLynRrcOwez4A1", - "payment_method_details": { - "card": { - "amount_authorized": 7200, - "brand": "visa", - "checks": { - "address_line1_check": null, - "address_postal_code_check": null, - "cvc_check": "pass" - }, - "country": "US", - "exp_month": 6, - "exp_year": 2033, - "extended_authorization": { - "status": "disabled" - }, - "fingerprint": "0VgUBpvqcUUnuSmK", - "funding": "credit", - "incremental_authorization": { - "status": "unavailable" - }, - "installments": null, - "last4": "4242", - "mandate": null, - "multicapture": { - "status": "unavailable" - }, - "network": "visa", - "network_token": { - "used": false - }, - "overcapture": { - "maximum_amount_capturable": 7200, - "status": "unavailable" - }, - "three_d_secure": null, - "wallet": null - }, - "type": "card" - }, - "receipt_email": "cturnbull@bitwarden.com", - "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/invoices/CAcaFwoVYWNjdF8xOXNtSVhJR0Juc0x5blJyKLSL1qgGMgYTnk_JOUA6LBY_SDEZNtuae1guQ6Dlcuev1TUHwn712t-UNnZdIc383zS15bXv_1dby8e4?s=ap", - "refunded": false, - "refunds": { - "object": "list", - "data": [ - ], - "has_more": false, - "total_count": 0, - "url": "/v1/charges/ch_3NvKgBIGBnsLynRr0ZyvP9AN/refunds" - }, - "review": null, - "shipping": null, - "source": null, - "source_transfer": null, - "statement_descriptor": null, - "statement_descriptor_suffix": null, - "status": "succeeded", - "transfer_data": null, - "transfer_group": null - } - }, - "livemode": false, - "pending_webhooks": 9, - "request": { - "id": "req_rig8N5Ca8EXYRy", - "idempotency_key": "db75068d-5d90-4c65-a410-4e2ed8347509" - }, - "type": "charge.succeeded" -} diff --git a/test/Billing.Test/Resources/Events/customer.subscription.updated.json b/test/Billing.Test/Resources/Events/customer.subscription.updated.json deleted file mode 100644 index 62a8590fa8..0000000000 --- a/test/Billing.Test/Resources/Events/customer.subscription.updated.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "id": "evt_1NvLMDIGBnsLynRr6oBxebrE", - "object": "event", - "api_version": "2024-06-20", - "created": 1695911902, - "data": { - "object": { - "id": "sub_1NvKoKIGBnsLynRrcLIAUWGf", - "object": "subscription", - "application": null, - "application_fee_percent": null, - "automatic_tax": { - "enabled": false - }, - "billing_cycle_anchor": 1695911900, - "billing_thresholds": null, - "cancel_at": null, - "cancel_at_period_end": false, - "canceled_at": null, - "cancellation_details": { - "comment": null, - "feedback": null, - "reason": null - }, - "collection_method": "charge_automatically", - "created": 1695909804, - "currency": "usd", - "current_period_end": 1727534300, - "current_period_start": 1695911900, - "customer": "cus_OimNNCC3RiI2HQ", - "days_until_due": null, - "default_payment_method": null, - "default_source": null, - "default_tax_rates": [ - ], - "description": null, - "discount": null, - "ended_at": null, - "items": { - "object": "list", - "data": [ - { - "id": "si_OimNgVtrESpqus", - "object": "subscription_item", - "billing_thresholds": null, - "created": 1695909805, - "metadata": { - }, - "plan": { - "id": "enterprise-org-seat-annually", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 3600, - "amount_decimal": "3600", - "billing_scheme": "per_unit", - "created": 1494268677, - "currency": "usd", - "interval": "year", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "2019 Enterprise Seat (Annually)", - "product": "prod_BUtogGemxnTi9z", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "price": { - "id": "enterprise-org-seat-annually", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1494268677, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "2019 Enterprise Seat (Annually)", - "product": "prod_BUtogGemxnTi9z", - "recurring": { - "aggregate_usage": null, - "interval": "year", - "interval_count": 1, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "unspecified", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 3600, - "unit_amount_decimal": "3600" - }, - "quantity": 1, - "subscription": "sub_1NvKoKIGBnsLynRrcLIAUWGf", - "tax_rates": [ - ] - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/subscription_items?subscription=sub_1NvKoKIGBnsLynRrcLIAUWGf" - }, - "latest_invoice": "in_1NvLM9IGBnsLynRrOysII07d", - "livemode": false, - "metadata": { - "organizationId": "84a569ea-4643-474a-83a9-b08b00e7a20d" - }, - "next_pending_invoice_item_invoice": null, - "on_behalf_of": null, - "pause_collection": null, - "payment_settings": { - "payment_method_options": null, - "payment_method_types": null, - "save_default_payment_method": "off" - }, - "pending_invoice_item_interval": null, - "pending_setup_intent": null, - "pending_update": null, - "plan": { - "id": "enterprise-org-seat-annually", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 3600, - "amount_decimal": "3600", - "billing_scheme": "per_unit", - "created": 1494268677, - "currency": "usd", - "interval": "year", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "2019 Enterprise Seat (Annually)", - "product": "prod_BUtogGemxnTi9z", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "quantity": 1, - "schedule": null, - "start_date": 1695909804, - "status": "active", - "test_clock": null, - "transfer_data": null, - "trial_end": 1695911899, - "trial_settings": { - "end_behavior": { - "missing_payment_method": "create_invoice" - } - }, - "trial_start": 1695909804 - }, - "previous_attributes": { - "billing_cycle_anchor": 1696514604, - "current_period_end": 1696514604, - "current_period_start": 1695909804, - "latest_invoice": "in_1NvKoKIGBnsLynRrSNRC6oYI", - "status": "trialing", - "trial_end": 1696514604 - } - }, - "livemode": false, - "pending_webhooks": 8, - "request": { - "id": "req_DMZPUU3BI66zAx", - "idempotency_key": "3fd8b4a5-6a20-46ab-9f45-b37b02a8017f" - }, - "type": "customer.subscription.updated" -} diff --git a/test/Billing.Test/Resources/Events/customer.updated.json b/test/Billing.Test/Resources/Events/customer.updated.json deleted file mode 100644 index 9aa0928515..0000000000 --- a/test/Billing.Test/Resources/Events/customer.updated.json +++ /dev/null @@ -1,311 +0,0 @@ -{ - "id": "evt_1NvKjSIGBnsLynRrS3MTK4DZ", - "object": "event", - "account": "acct_19smIXIGBnsLynRr", - "api_version": "2024-06-20", - "created": 1695909502, - "data": { - "object": { - "id": "cus_Of54kUr3gV88lM", - "object": "customer", - "address": { - "city": null, - "country": "US", - "line1": "", - "line2": null, - "postal_code": "33701", - "state": null - }, - "balance": 0, - "created": 1695056798, - "currency": "usd", - "default_source": "src_1NtAfeIGBnsLynRrYDrceax7", - "delinquent": false, - "description": "Premium User", - "discount": null, - "email": "premium@bitwarden.com", - "invoice_prefix": "C506E8CE", - "invoice_settings": { - "custom_fields": [ - { - "name": "Subscriber", - "value": "Premium User" - } - ], - "default_payment_method": "pm_1Nrku9IGBnsLynRrcsQ3hy6C", - "footer": null, - "rendering_options": null - }, - "livemode": false, - "metadata": { - "region": "US" - }, - "name": null, - "next_invoice_sequence": 2, - "phone": null, - "preferred_locales": [ - ], - "shipping": null, - "tax_exempt": "none", - "test_clock": null, - "account_balance": 0, - "cards": { - "object": "list", - "data": [ - ], - "has_more": false, - "total_count": 0, - "url": "/v1/customers/cus_Of54kUr3gV88lM/cards" - }, - "default_card": null, - "default_currency": "usd", - "sources": { - "object": "list", - "data": [ - { - "id": "src_1NtAfeIGBnsLynRrYDrceax7", - "object": "source", - "ach_credit_transfer": { - "account_number": "test_b2d1c6415f6f", - "routing_number": "110000000", - "fingerprint": "ePO4hBQanSft3gvU", - "swift_code": "TSTEZ122", - "bank_name": "TEST BANK", - "refund_routing_number": null, - "refund_account_holder_type": null, - "refund_account_holder_name": null - }, - "amount": null, - "client_secret": "src_client_secret_bUAP2uDRw6Pwj0xYk32LmJ3K", - "created": 1695394170, - "currency": "usd", - "customer": "cus_Of54kUr3gV88lM", - "flow": "receiver", - "livemode": false, - "metadata": { - }, - "owner": { - "address": null, - "email": "amount_0@stripe.com", - "name": null, - "phone": null, - "verified_address": null, - "verified_email": null, - "verified_name": null, - "verified_phone": null - }, - "receiver": { - "address": "110000000-test_b2d1c6415f6f", - "amount_charged": 0, - "amount_received": 0, - "amount_returned": 0, - "refund_attributes_method": "email", - "refund_attributes_status": "missing" - }, - "statement_descriptor": null, - "status": "pending", - "type": "ach_credit_transfer", - "usage": "reusable" - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/customers/cus_Of54kUr3gV88lM/sources" - }, - "subscriptions": { - "object": "list", - "data": [ - { - "id": "sub_1NrkuBIGBnsLynRrzjFGIjEw", - "object": "subscription", - "application": null, - "application_fee_percent": null, - "automatic_tax": { - "enabled": false - }, - "billing": "charge_automatically", - "billing_cycle_anchor": 1695056799, - "billing_thresholds": null, - "cancel_at": null, - "cancel_at_period_end": false, - "canceled_at": null, - "cancellation_details": { - "comment": null, - "feedback": null, - "reason": null - }, - "collection_method": "charge_automatically", - "created": 1695056799, - "currency": "usd", - "current_period_end": 1726679199, - "current_period_start": 1695056799, - "customer": "cus_Of54kUr3gV88lM", - "days_until_due": null, - "default_payment_method": null, - "default_source": null, - "default_tax_rates": [ - ], - "description": null, - "discount": null, - "ended_at": null, - "invoice_customer_balance_settings": { - "consume_applied_balance_on_void": true - }, - "items": { - "object": "list", - "data": [ - { - "id": "si_Of54i3aK9I5Wro", - "object": "subscription_item", - "billing_thresholds": null, - "created": 1695056800, - "metadata": { - }, - "plan": { - "id": "premium-annually", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 1000, - "amount_decimal": "1000", - "billing_scheme": "per_unit", - "created": 1499289328, - "currency": "usd", - "interval": "year", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "name": "Premium (Annually)", - "nickname": "Premium (Annually)", - "product": "prod_BUqgYr48VzDuCg", - "statement_description": null, - "statement_descriptor": null, - "tiers": null, - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "price": { - "id": "premium-annually", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1499289328, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "Premium (Annually)", - "product": "prod_BUqgYr48VzDuCg", - "recurring": { - "aggregate_usage": null, - "interval": "year", - "interval_count": 1, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "unspecified", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 1000, - "unit_amount_decimal": "1000" - }, - "quantity": 1, - "subscription": "sub_1NrkuBIGBnsLynRrzjFGIjEw", - "tax_rates": [ - ] - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/subscription_items?subscription=sub_1NrkuBIGBnsLynRrzjFGIjEw" - }, - "latest_invoice": "in_1NrkuBIGBnsLynRr40gyJTVU", - "livemode": false, - "metadata": { - "userId": "91f40b6d-ac3b-4348-804b-b0810119ac6a" - }, - "next_pending_invoice_item_invoice": null, - "on_behalf_of": null, - "pause_collection": null, - "payment_settings": { - "payment_method_options": null, - "payment_method_types": null, - "save_default_payment_method": "off" - }, - "pending_invoice_item_interval": null, - "pending_setup_intent": null, - "pending_update": null, - "plan": { - "id": "premium-annually", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 1000, - "amount_decimal": "1000", - "billing_scheme": "per_unit", - "created": 1499289328, - "currency": "usd", - "interval": "year", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "name": "Premium (Annually)", - "nickname": "Premium (Annually)", - "product": "prod_BUqgYr48VzDuCg", - "statement_description": null, - "statement_descriptor": null, - "tiers": null, - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "quantity": 1, - "schedule": null, - "start": 1695056799, - "start_date": 1695056799, - "status": "active", - "tax_percent": null, - "test_clock": null, - "transfer_data": null, - "trial_end": null, - "trial_settings": { - "end_behavior": { - "missing_payment_method": "create_invoice" - } - }, - "trial_start": null - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/customers/cus_Of54kUr3gV88lM/subscriptions" - }, - "tax_ids": { - "object": "list", - "data": [ - ], - "has_more": false, - "total_count": 0, - "url": "/v1/customers/cus_Of54kUr3gV88lM/tax_ids" - }, - "tax_info": null, - "tax_info_verification": null - }, - "previous_attributes": { - "email": "premium-new@bitwarden.com" - } - }, - "livemode": false, - "pending_webhooks": 5, - "request": "req_2RtGdXCfiicFLx", - "type": "customer.updated", - "user_id": "acct_19smIXIGBnsLynRr" -} diff --git a/test/Billing.Test/Resources/Events/invoice.created.json b/test/Billing.Test/Resources/Events/invoice.created.json deleted file mode 100644 index bf53372b51..0000000000 --- a/test/Billing.Test/Resources/Events/invoice.created.json +++ /dev/null @@ -1,222 +0,0 @@ -{ - "id": "evt_1NvKzfIGBnsLynRr0SkwrlkE", - "object": "event", - "api_version": "2024-06-20", - "created": 1695910506, - "data": { - "object": { - "id": "in_1NvKzdIGBnsLynRr8fE8cpbg", - "object": "invoice", - "account_country": "US", - "account_name": "Bitwarden Inc.", - "account_tax_ids": null, - "amount_due": 0, - "amount_paid": 0, - "amount_remaining": 0, - "amount_shipping": 0, - "application": null, - "application_fee_amount": null, - "attempt_count": 0, - "attempted": true, - "auto_advance": false, - "automatic_tax": { - "enabled": false, - "status": null - }, - "billing_reason": "subscription_create", - "charge": null, - "collection_method": "charge_automatically", - "created": 1695910505, - "currency": "usd", - "custom_fields": [ - { - "name": "Organization", - "value": "teams 2023 monthly - 2" - } - ], - "customer": "cus_OimYrxnMTMMK1E", - "customer_address": { - "city": null, - "country": "US", - "line1": "", - "line2": null, - "postal_code": "12345", - "state": null - }, - "customer_email": "cturnbull@bitwarden.com", - "customer_name": null, - "customer_phone": null, - "customer_shipping": null, - "customer_tax_exempt": "none", - "customer_tax_ids": [ - ], - "default_payment_method": null, - "default_source": null, - "default_tax_rates": [ - ], - "description": null, - "discount": null, - "discounts": [ - ], - "due_date": null, - "effective_at": 1695910505, - "ending_balance": 0, - "footer": null, - "from_invoice": null, - "hosted_invoice_url": "https://invoice.stripe.com/i/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9PaW1ZVlo4dFRtbkNQQVY5aHNpckQxN1QzRHBPcVBOLDg2NDUxMzA30200etYRHca2?s=ap", - "invoice_pdf": "https://pay.stripe.com/invoice/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9PaW1ZVlo4dFRtbkNQQVY5aHNpckQxN1QzRHBPcVBOLDg2NDUxMzA30200etYRHca2/pdf?s=ap", - "last_finalization_error": null, - "latest_revision": null, - "lines": { - "object": "list", - "data": [ - { - "id": "il_1NvKzdIGBnsLynRr2pS4ZA8e", - "object": "line_item", - "amount": 0, - "amount_excluding_tax": 0, - "currency": "usd", - "description": "Trial period for Teams Organization Seat", - "discount_amounts": [ - ], - "discountable": true, - "discounts": [ - ], - "livemode": false, - "metadata": { - "organizationId": "3fbc84ce-102d-4919-b89b-b08b00ead71a" - }, - "period": { - "end": 1696515305, - "start": 1695910505 - }, - "plan": { - "id": "2020-teams-org-seat-monthly", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 400, - "amount_decimal": "400", - "billing_scheme": "per_unit", - "created": 1595263113, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "Teams Organization Seat (Monthly) 2023", - "product": "prod_HgOooYXDr2DDAA", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "price": { - "id": "2020-teams-org-seat-monthly", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1595263113, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "Teams Organization Seat (Monthly) 2023", - "product": "prod_HgOooYXDr2DDAA", - "recurring": { - "aggregate_usage": null, - "interval": "month", - "interval_count": 1, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "unspecified", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 400, - "unit_amount_decimal": "400" - }, - "proration": false, - "proration_details": { - "credited_items": null - }, - "quantity": 1, - "subscription": "sub_1NvKzdIGBnsLynRrKIHQamZc", - "subscription_item": "si_OimYNSbvuqdtTr", - "tax_amounts": [ - ], - "tax_rates": [ - ], - "type": "subscription", - "unit_amount_excluding_tax": "0" - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/invoices/in_1NvKzdIGBnsLynRr8fE8cpbg/lines" - }, - "livemode": false, - "metadata": { - }, - "next_payment_attempt": null, - "number": "3E96D078-0001", - "on_behalf_of": null, - "paid": true, - "paid_out_of_band": false, - "payment_intent": null, - "payment_settings": { - "default_mandate": null, - "payment_method_options": null, - "payment_method_types": null - }, - "period_end": 1695910505, - "period_start": 1695910505, - "post_payment_credit_notes_amount": 0, - "pre_payment_credit_notes_amount": 0, - "quote": null, - "receipt_number": null, - "rendering": null, - "rendering_options": null, - "shipping_cost": null, - "shipping_details": null, - "starting_balance": 0, - "statement_descriptor": null, - "status": "paid", - "status_transitions": { - "finalized_at": 1695910505, - "marked_uncollectible_at": null, - "paid_at": 1695910505, - "voided_at": null - }, - "subscription": "sub_1NvKzdIGBnsLynRrKIHQamZc", - "subscription_details": { - "metadata": { - "organizationId": "3fbc84ce-102d-4919-b89b-b08b00ead71a" - } - }, - "subtotal": 0, - "subtotal_excluding_tax": 0, - "tax": null, - "test_clock": null, - "total": 0, - "total_discount_amounts": [ - ], - "total_excluding_tax": 0, - "total_tax_amounts": [ - ], - "transfer_data": null, - "webhooks_delivered_at": null - } - }, - "livemode": false, - "pending_webhooks": 8, - "request": { - "id": "req_roIwONfgyfZdr4", - "idempotency_key": "dd2a171b-b9c7-4d2d-89d5-1ceae3c0595d" - }, - "type": "invoice.created" -} diff --git a/test/Billing.Test/Resources/Events/invoice.finalized.json b/test/Billing.Test/Resources/Events/invoice.finalized.json deleted file mode 100644 index 207fab497e..0000000000 --- a/test/Billing.Test/Resources/Events/invoice.finalized.json +++ /dev/null @@ -1,400 +0,0 @@ -{ - "id": "evt_1PQaABIGBnsLynRrhoJjGnyz", - "object": "event", - "account": "acct_19smIXIGBnsLynRr", - "api_version": "2024-06-20", - "created": 1718133319, - "data": { - "object": { - "id": "in_1PQa9fIGBnsLynRraYIqTdBs", - "object": "invoice", - "account_country": "US", - "account_name": "Bitwarden Inc.", - "account_tax_ids": null, - "amount_due": 84240, - "amount_paid": 0, - "amount_remaining": 84240, - "amount_shipping": 0, - "application": null, - "attempt_count": 0, - "attempted": false, - "auto_advance": true, - "automatic_tax": { - "enabled": true, - "liability": { - "type": "self" - }, - "status": "complete" - }, - "billing_reason": "subscription_update", - "charge": null, - "collection_method": "send_invoice", - "created": 1718133291, - "currency": "usd", - "custom_fields": [ - { - "name": "Provider", - "value": "MSP" - } - ], - "customer": "cus_QH8QVKyTh2lfcG", - "customer_address": { - "city": null, - "country": "US", - "line1": null, - "line2": null, - "postal_code": "12345", - "state": null - }, - "customer_email": "billing@msp.com", - "customer_name": null, - "customer_phone": null, - "customer_shipping": null, - "customer_tax_exempt": "none", - "customer_tax_ids": [ - ], - "default_payment_method": null, - "default_source": null, - "default_tax_rates": [ - ], - "description": null, - "discount": { - "id": "di_1PQa9eIGBnsLynRrwwYr2bGD", - "object": "discount", - "checkout_session": null, - "coupon": { - "id": "msp-discount-35", - "object": "coupon", - "amount_off": null, - "created": 1678805729, - "currency": null, - "duration": "forever", - "duration_in_months": null, - "livemode": false, - "max_redemptions": null, - "metadata": { - }, - "name": "MSP Discount - 35%", - "percent_off": 35, - "redeem_by": null, - "times_redeemed": 515, - "valid": true, - "percent_off_precise": 35 - }, - "customer": "cus_QH8QVKyTh2lfcG", - "end": null, - "invoice": null, - "invoice_item": null, - "promotion_code": null, - "start": 1718133290, - "subscription": null, - "subscription_item": null - }, - "discounts": [ - "di_1PQa9eIGBnsLynRrwwYr2bGD" - ], - "due_date": 1720725291, - "effective_at": 1718136893, - "ending_balance": 0, - "footer": null, - "from_invoice": null, - "hosted_invoice_url": "https://invoice.stripe.com/i/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9RSDhRYVNIejNDMXBMVXAzM0M3S2RwaUt1Z3NuVHVzLDEwODY3NDEyMg0200RT8cC2nw?s=ap", - "invoice_pdf": "https://pay.stripe.com/invoice/acct_19smIXIGBnsLynRr/test_YWNjdF8xOXNtSVhJR0Juc0x5blJyLF9RSDhRYVNIejNDMXBMVXAzM0M3S2RwaUt1Z3NuVHVzLDEwODY3NDEyMg0200RT8cC2nw/pdf?s=ap", - "issuer": { - "type": "self" - }, - "last_finalization_error": null, - "latest_revision": null, - "lines": { - "object": "list", - "data": [ - { - "id": "sub_1PQa9fIGBnsLynRr83lNrFHa", - "object": "line_item", - "amount": 50000, - "amount_excluding_tax": 50000, - "currency": "usd", - "description": null, - "discount_amounts": [ - { - "amount": 17500, - "discount": "di_1PQa9eIGBnsLynRrwwYr2bGD" - } - ], - "discountable": true, - "discounts": [ - ], - "invoice": "in_1PQa9fIGBnsLynRraYIqTdBs", - "livemode": false, - "metadata": { - }, - "period": { - "end": 1720725291, - "start": 1718133291 - }, - "plan": { - "id": "2023-teams-org-seat-monthly", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 500, - "amount_decimal": "500", - "billing_scheme": "per_unit", - "created": 1695839010, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "meter": null, - "nickname": "Teams Organization Seat (Monthly)", - "product": "prod_HgOooYXDr2DDAA", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed", - "name": "Password Manager - Teams Plan", - "statement_description": null, - "statement_descriptor": null, - "tiers": null - }, - "price": { - "id": "2023-teams-org-seat-monthly", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1695839010, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "Teams Organization Seat (Monthly)", - "product": "prod_HgOooYXDr2DDAA", - "recurring": { - "aggregate_usage": null, - "interval": "month", - "interval_count": 1, - "meter": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "exclusive", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 500, - "unit_amount_decimal": "500" - }, - "proration": false, - "proration_details": { - "credited_items": null - }, - "quantity": 100, - "subscription": null, - "subscription_item": "si_QH8Qo4WEJxOVwx", - "tax_amounts": [ - { - "amount": 2600, - "inclusive": false, - "tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC", - "taxability_reason": "standard_rated", - "taxable_amount": 32500 - } - ], - "tax_rates": [ - ], - "type": "subscription", - "unit_amount_excluding_tax": "500", - "unique_id": "il_1PQa9fIGBnsLynRrSJ3cxrdU", - "unique_line_item_id": "sli_1acb3eIGBnsLynRr4b9c2f48" - }, - { - "id": "sub_1PQa9fIGBnsLynRr83lNrFHa", - "object": "line_item", - "amount": 70000, - "amount_excluding_tax": 70000, - "currency": "usd", - "description": null, - "discount_amounts": [ - { - "amount": 24500, - "discount": "di_1PQa9eIGBnsLynRrwwYr2bGD" - } - ], - "discountable": true, - "discounts": [ - ], - "invoice": "in_1PQa9fIGBnsLynRraYIqTdBs", - "livemode": false, - "metadata": { - }, - "period": { - "end": 1720725291, - "start": 1718133291 - }, - "plan": { - "id": "2023-enterprise-seat-monthly", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 700, - "amount_decimal": "700", - "billing_scheme": "per_unit", - "created": 1695152194, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "meter": null, - "nickname": "Enterprise Organization (Monthly)", - "product": "prod_HgSOgzUlYDFOzf", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed", - "name": "Password Manager - Enterprise Plan", - "statement_description": null, - "statement_descriptor": null, - "tiers": null - }, - "price": { - "id": "2023-enterprise-seat-monthly", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1695152194, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "Enterprise Organization (Monthly)", - "product": "prod_HgSOgzUlYDFOzf", - "recurring": { - "aggregate_usage": null, - "interval": "month", - "interval_count": 1, - "meter": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "exclusive", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 700, - "unit_amount_decimal": "700" - }, - "proration": false, - "proration_details": { - "credited_items": null - }, - "quantity": 100, - "subscription": null, - "subscription_item": "si_QH8QUjtceXvcis", - "tax_amounts": [ - { - "amount": 3640, - "inclusive": false, - "tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC", - "taxability_reason": "standard_rated", - "taxable_amount": 45500 - } - ], - "tax_rates": [ - ], - "type": "subscription", - "unit_amount_excluding_tax": "700", - "unique_id": "il_1PQa9fIGBnsLynRrVviet37m", - "unique_line_item_id": "sli_11b229IGBnsLynRr837b79d0" - } - ], - "has_more": false, - "total_count": 2, - "url": "/v1/invoices/in_1PQa9fIGBnsLynRraYIqTdBs/lines" - }, - "livemode": false, - "metadata": { - }, - "next_payment_attempt": null, - "number": "525EB050-0001", - "on_behalf_of": null, - "paid": false, - "paid_out_of_band": false, - "payment_intent": "pi_3PQaA7IGBnsLynRr1swr9XJE", - "payment_settings": { - "default_mandate": null, - "payment_method_options": null, - "payment_method_types": null - }, - "period_end": 1718133291, - "period_start": 1718133291, - "post_payment_credit_notes_amount": 0, - "pre_payment_credit_notes_amount": 0, - "quote": null, - "receipt_number": null, - "rendering": null, - "rendering_options": null, - "shipping_cost": null, - "shipping_details": null, - "starting_balance": 0, - "statement_descriptor": null, - "status": "open", - "status_transitions": { - "finalized_at": 1718136893, - "marked_uncollectible_at": null, - "paid_at": null, - "voided_at": null - }, - "subscription": "sub_1PQa9fIGBnsLynRr83lNrFHa", - "subscription_details": { - "metadata": { - "providerId": "655bc5a3-2332-4201-a9a6-b18c013d0572" - } - }, - "subtotal": 120000, - "subtotal_excluding_tax": 120000, - "tax": 6240, - "test_clock": "clock_1PQaA4IGBnsLynRrptkZjgxc", - "total": 84240, - "total_discount_amounts": [ - { - "amount": 42000, - "discount": "di_1PQa9eIGBnsLynRrwwYr2bGD" - } - ], - "total_excluding_tax": 78000, - "total_tax_amounts": [ - { - "amount": 6240, - "inclusive": false, - "tax_rate": "txr_1OZyBuIGBnsLynRrX0PJLuMC", - "taxability_reason": "standard_rated", - "taxable_amount": 78000 - } - ], - "transfer_data": null, - "webhooks_delivered_at": 1718133293, - "application_fee": null, - "billing": "send_invoice", - "closed": false, - "date": 1718133291, - "finalized_at": 1718136893, - "forgiven": false, - "payment": null, - "statement_description": null, - "tax_percent": 8 - } - }, - "livemode": false, - "pending_webhooks": 5, - "request": null, - "type": "invoice.finalized", - "user_id": "acct_19smIXIGBnsLynRr" -} diff --git a/test/Billing.Test/Resources/Events/invoice.upcoming.json b/test/Billing.Test/Resources/Events/invoice.upcoming.json deleted file mode 100644 index 1ecf2c616d..0000000000 --- a/test/Billing.Test/Resources/Events/invoice.upcoming.json +++ /dev/null @@ -1,225 +0,0 @@ -{ - "id": "evt_1Nv0w8IGBnsLynRrZoDVI44u", - "object": "event", - "api_version": "2024-06-20", - "created": 1695833408, - "data": { - "object": { - "object": "invoice", - "account_country": "US", - "account_name": "Bitwarden Inc.", - "account_tax_ids": null, - "amount_due": 0, - "amount_paid": 0, - "amount_remaining": 0, - "amount_shipping": 0, - "application": null, - "application_fee_amount": null, - "attempt_count": 0, - "attempted": false, - "automatic_tax": { - "enabled": true, - "status": "complete" - }, - "billing_reason": "upcoming", - "charge": null, - "collection_method": "charge_automatically", - "created": 1697128681, - "currency": "usd", - "custom_fields": null, - "customer": "cus_M8DV9wiyNa2JxQ", - "customer_address": { - "city": null, - "country": "US", - "line1": "", - "line2": null, - "postal_code": "90019", - "state": null - }, - "customer_email": "vphan@bitwarden.com", - "customer_name": null, - "customer_phone": null, - "customer_shipping": null, - "customer_tax_exempt": "none", - "customer_tax_ids": [ - ], - "default_payment_method": null, - "default_source": null, - "default_tax_rates": [ - ], - "description": null, - "discount": null, - "discounts": [ - ], - "due_date": null, - "effective_at": null, - "ending_balance": -6779, - "footer": null, - "from_invoice": null, - "last_finalization_error": null, - "latest_revision": null, - "lines": { - "object": "list", - "data": [ - { - "id": "il_tmp_12b5e8IGBnsLynRr1996ac3a", - "object": "line_item", - "amount": 2000, - "amount_excluding_tax": 2000, - "currency": "usd", - "description": "5 × 2019 Enterprise Seat (Monthly) (at $4.00 / month)", - "discount_amounts": [ - ], - "discountable": true, - "discounts": [ - ], - "livemode": false, - "metadata": { - }, - "period": { - "end": 1699807081, - "start": 1697128681 - }, - "plan": { - "id": "enterprise-org-seat-monthly", - "object": "plan", - "active": true, - "aggregate_usage": null, - "amount": 400, - "amount_decimal": "400", - "billing_scheme": "per_unit", - "created": 1494268635, - "currency": "usd", - "interval": "month", - "interval_count": 1, - "livemode": false, - "metadata": { - }, - "nickname": "2019 Enterprise Seat (Monthly)", - "product": "prod_BVButYytPSlgs6", - "tiers_mode": null, - "transform_usage": null, - "trial_period_days": null, - "usage_type": "licensed" - }, - "price": { - "id": "enterprise-org-seat-monthly", - "object": "price", - "active": true, - "billing_scheme": "per_unit", - "created": 1494268635, - "currency": "usd", - "custom_unit_amount": null, - "livemode": false, - "lookup_key": null, - "metadata": { - }, - "nickname": "2019 Enterprise Seat (Monthly)", - "product": "prod_BVButYytPSlgs6", - "recurring": { - "aggregate_usage": null, - "interval": "month", - "interval_count": 1, - "trial_period_days": null, - "usage_type": "licensed" - }, - "tax_behavior": "unspecified", - "tiers_mode": null, - "transform_quantity": null, - "type": "recurring", - "unit_amount": 400, - "unit_amount_decimal": "400" - }, - "proration": false, - "proration_details": { - "credited_items": null - }, - "quantity": 5, - "subscription": "sub_1NQxz4IGBnsLynRr1KbitG7v", - "subscription_item": "si_ODOmLnPDHBuMxX", - "tax_amounts": [ - { - "amount": 0, - "inclusive": false, - "tax_rate": "txr_1N6XCyIGBnsLynRr0LHs4AUD", - "taxability_reason": "product_exempt", - "taxable_amount": 0 - } - ], - "tax_rates": [ - ], - "type": "subscription", - "unit_amount_excluding_tax": "400" - } - ], - "has_more": false, - "total_count": 1, - "url": "/v1/invoices/upcoming/lines?customer=cus_M8DV9wiyNa2JxQ&subscription=sub_1NQxz4IGBnsLynRr1KbitG7v" - }, - "livemode": false, - "metadata": { - }, - "next_payment_attempt": 1697132281, - "number": null, - "on_behalf_of": null, - "paid": false, - "paid_out_of_band": false, - "payment_intent": null, - "payment_settings": { - "default_mandate": null, - "payment_method_options": null, - "payment_method_types": null - }, - "period_end": 1697128681, - "period_start": 1694536681, - "post_payment_credit_notes_amount": 0, - "pre_payment_credit_notes_amount": 0, - "quote": null, - "receipt_number": null, - "rendering": null, - "rendering_options": null, - "shipping_cost": null, - "shipping_details": null, - "starting_balance": -8779, - "statement_descriptor": null, - "status": "draft", - "status_transitions": { - "finalized_at": null, - "marked_uncollectible_at": null, - "paid_at": null, - "voided_at": null - }, - "subscription": "sub_1NQxz4IGBnsLynRr1KbitG7v", - "subscription_details": { - "metadata": { - } - }, - "subtotal": 2000, - "subtotal_excluding_tax": 2000, - "tax": 0, - "test_clock": null, - "total": 2000, - "total_discount_amounts": [ - ], - "total_excluding_tax": 2000, - "total_tax_amounts": [ - { - "amount": 0, - "inclusive": false, - "tax_rate": "txr_1N6XCyIGBnsLynRr0LHs4AUD", - "taxability_reason": "product_exempt", - "taxable_amount": 0 - } - ], - "transfer_data": null, - "webhooks_delivered_at": null - } - }, - "livemode": false, - "pending_webhooks": 5, - "request": { - "id": null, - "idempotency_key": null - }, - "type": "invoice.upcoming" -} diff --git a/test/Billing.Test/Resources/Events/payment_method.attached.json b/test/Billing.Test/Resources/Events/payment_method.attached.json deleted file mode 100644 index 2d22a929d4..0000000000 --- a/test/Billing.Test/Resources/Events/payment_method.attached.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "id": "evt_1NvKzcIGBnsLynRrPJ3hybkd", - "object": "event", - "api_version": "2024-06-20", - "created": 1695910504, - "data": { - "object": { - "id": "pm_1NvKzbIGBnsLynRry6x7Buvc", - "object": "payment_method", - "billing_details": { - "address": { - "city": null, - "country": null, - "line1": null, - "line2": null, - "postal_code": null, - "state": null - }, - "email": null, - "name": null, - "phone": null - }, - "card": { - "brand": "visa", - "checks": { - "address_line1_check": null, - "address_postal_code_check": null, - "cvc_check": "pass" - }, - "country": "US", - "exp_month": 6, - "exp_year": 2033, - "fingerprint": "0VgUBpvqcUUnuSmK", - "funding": "credit", - "generated_from": null, - "last4": "4242", - "networks": { - "available": [ - "visa" - ], - "preferred": null - }, - "three_d_secure_usage": { - "supported": true - }, - "wallet": null - }, - "created": 1695910503, - "customer": "cus_OimYrxnMTMMK1E", - "livemode": false, - "metadata": { - }, - "type": "card" - } - }, - "livemode": false, - "pending_webhooks": 7, - "request": { - "id": "req_2WslNSBD9wAV5v", - "idempotency_key": "db1a648a-3445-47b3-a403-9f3d1303a880" - }, - "type": "payment_method.attached" -} diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index 7d95157bd2..d5f273fa65 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -1,6 +1,5 @@ using Bit.Billing.Services; using Bit.Billing.Services.Implementations; -using Bit.Billing.Test.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; @@ -59,29 +58,69 @@ public class ProviderEventServiceTests public async Task TryRecordInvoiceLineItems_EventTypeNotInvoiceCreatedOrInvoiceFinalized_NoOp() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached); + var stripeEvent = new Event { Type = "payment_method.attached" }; // Act await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); // Assert - await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any()); + await _stripeEventService.DidNotReceiveWithAnyArgs().GetInvoice(Arg.Any(), Arg.Any(), Arg.Any?>()); + } + + [Fact] + public async Task TryRecordInvoiceLineItems_InvoiceParentTypeNotSubscriptionDetails_NoOp() + { + // Arrange + var stripeEvent = new Event + { + Type = "invoice.created" + }; + + var invoice = new Invoice + { + Parent = new InvoiceParent + { + Type = "credit_note", + SubscriptionDetails = new InvoiceParentSubscriptionDetails + { + SubscriptionId = "sub_1" + } + } + }; + + _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any?>()).Returns(invoice); + + // Act + await _providerEventService.TryRecordInvoiceLineItems(stripeEvent); + + // Assert + await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription(Arg.Any()); } [Fact] public async Task TryRecordInvoiceLineItems_EventNotProviderRelated_NoOp() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var stripeEvent = new Event + { + Type = "invoice.created" + }; const string subscriptionId = "sub_1"; var invoice = new Invoice { - SubscriptionId = subscriptionId + Parent = new InvoiceParent + { + Type = "subscription_details", + SubscriptionDetails = new InvoiceParentSubscriptionDetails + { + SubscriptionId = subscriptionId + } + } }; - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); + _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any?>()).Returns(invoice); var subscription = new Subscription { @@ -101,7 +140,10 @@ public class ProviderEventServiceTests public async Task TryRecordInvoiceLineItems_InvoiceCreated_Succeeds() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var stripeEvent = new Event + { + Type = "invoice.created" + }; const string subscriptionId = "sub_1"; var providerId = Guid.NewGuid(); @@ -110,17 +152,26 @@ public class ProviderEventServiceTests { Id = "invoice_1", Number = "A", - SubscriptionId = subscriptionId, - Discount = new Discount + Parent = new InvoiceParent { - Coupon = new Coupon + Type = "subscription_details", + SubscriptionDetails = new InvoiceParentSubscriptionDetails { - PercentOff = 35 + SubscriptionId = subscriptionId } - } + }, + Discounts = [ + new Discount + { + Coupon = new Coupon + { + PercentOff = 35 + } + } + ] }; - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); + _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any?>()).Returns(invoice); var subscription = new Subscription { @@ -249,7 +300,10 @@ public class ProviderEventServiceTests public async Task TryRecordInvoiceLineItems_InvoiceFinalized_Succeeds() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceFinalized); + var stripeEvent = new Event + { + Type = "invoice.finalized" + }; const string subscriptionId = "sub_1"; var providerId = Guid.NewGuid(); @@ -258,10 +312,17 @@ public class ProviderEventServiceTests { Id = "invoice_1", Number = "A", - SubscriptionId = subscriptionId + Parent = new InvoiceParent + { + Type = "subscription_details", + SubscriptionDetails = new InvoiceParentSubscriptionDetails + { + SubscriptionId = subscriptionId + } + }, }; - _stripeEventService.GetInvoice(stripeEvent).Returns(invoice); + _stripeEventService.GetInvoice(stripeEvent, true, Arg.Any?>()).Returns(invoice); var subscription = new Subscription { diff --git a/test/Billing.Test/Services/SubscriptionDeletedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionDeletedHandlerTests.cs index 2797b2e589..78dc5aa791 100644 --- a/test/Billing.Test/Services/SubscriptionDeletedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionDeletedHandlerTests.cs @@ -2,6 +2,7 @@ using Bit.Billing.Services; using Bit.Billing.Services.Implementations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Billing.Extensions; using Bit.Core.Services; using NSubstitute; using Stripe; @@ -38,7 +39,13 @@ public class SubscriptionDeletedHandlerTests var subscription = new Subscription { Status = "active", - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Metadata = new Dictionary() }; @@ -63,11 +70,14 @@ public class SubscriptionDeletedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Canceled, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), - Metadata = new Dictionary + Items = new StripeList { - { "organizationId", organizationId.ToString() } - } + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, + Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription); @@ -79,7 +89,7 @@ public class SubscriptionDeletedHandlerTests // Assert await _organizationDisableCommand.Received(1) - .DisableAsync(organizationId, subscription.CurrentPeriodEnd); + .DisableAsync(organizationId, subscription.GetCurrentPeriodEnd()); } [Fact] @@ -91,11 +101,14 @@ public class SubscriptionDeletedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Canceled, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), - Metadata = new Dictionary + Items = new StripeList { - { "userId", userId.ToString() } - } + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, + Metadata = new Dictionary { { "userId", userId.ToString() } } }; _stripeEventService.GetSubscription(stripeEvent, true).Returns(subscription); @@ -107,7 +120,7 @@ public class SubscriptionDeletedHandlerTests // Assert await _userService.Received(1) - .DisablePremiumAsync(userId, subscription.CurrentPeriodEnd); + .DisablePremiumAsync(userId, subscription.GetCurrentPeriodEnd()); } [Fact] @@ -119,11 +132,14 @@ public class SubscriptionDeletedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Canceled, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), - Metadata = new Dictionary + Items = new StripeList { - { "organizationId", organizationId.ToString() } + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] }, + Metadata = new Dictionary { { "organizationId", organizationId.ToString() } }, CancellationDetails = new SubscriptionCancellationDetails { Comment = "Cancelled as part of provider migration to Consolidated Billing" @@ -151,11 +167,14 @@ public class SubscriptionDeletedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Canceled, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), - Metadata = new Dictionary + Items = new StripeList { - { "organizationId", organizationId.ToString() } + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] }, + Metadata = new Dictionary { { "organizationId", organizationId.ToString() } }, CancellationDetails = new SubscriptionCancellationDetails { Comment = "Organization was added to Provider" diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index 6a7cd7704b..16287bc5c9 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -96,7 +96,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; @@ -142,7 +148,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Metadata = new Dictionary { ["providerId"] = providerId.ToString(), @@ -206,7 +218,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Metadata = new Dictionary { ["providerId"] = providerId.ToString() }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }, TestClock = null @@ -257,6 +275,13 @@ public class SubscriptionUpdatedHandlerTests var subscription = new Subscription { Id = subscriptionId, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Status = StripeSubscriptionStatus.Unpaid, Metadata = new Dictionary { { "providerId", providerId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } @@ -306,6 +331,13 @@ public class SubscriptionUpdatedHandlerTests var subscription = new Subscription { Id = subscriptionId, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Status = StripeSubscriptionStatus.Unpaid, Metadata = new Dictionary { { "providerId", providerId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } @@ -348,7 +380,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.IncompleteExpired, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "providerId", providerId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "renewal" } }; @@ -390,7 +428,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "providerId", providerId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; @@ -426,7 +470,13 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "providerId", providerId.ToString() } }, LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; @@ -464,13 +514,16 @@ public class SubscriptionUpdatedHandlerTests { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - CurrentPeriodEnd = currentPeriodEnd, Metadata = new Dictionary { { "userId", userId.ToString() } }, Items = new StripeList { Data = [ - new SubscriptionItem { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } } + new SubscriptionItem + { + CurrentPeriodEnd = currentPeriodEnd, + Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } + } ] } }; @@ -508,7 +561,13 @@ public class SubscriptionUpdatedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; @@ -552,7 +611,13 @@ public class SubscriptionUpdatedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "userId", userId.ToString() } } }; @@ -583,7 +648,13 @@ public class SubscriptionUpdatedHandlerTests var subscription = new Subscription { Status = StripeSubscriptionStatus.Active, - CurrentPeriodEnd = currentPeriodEnd, + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + }, Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; @@ -616,18 +687,24 @@ public class SubscriptionUpdatedHandlerTests { Id = "sub_123", Status = StripeSubscriptionStatus.Active, - CurrentPeriodEnd = DateTime.UtcNow.AddDays(10), CustomerId = "cus_123", Items = new StripeList { - Data = [new SubscriptionItem { Plan = new Plan { Id = "2023-enterprise-org-seat-annually" } }] + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(10), + Plan = new Plan { Id = "2023-enterprise-org-seat-annually" } + } + ] }, Customer = new Customer { Balance = 0, Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } } }, - Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }, + Discounts = [new Discount { Coupon = new Coupon { Id = "sm-standalone" } }], Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; @@ -728,7 +805,6 @@ public class SubscriptionUpdatedHandlerTests .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); } - [Fact] public async Task HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_EnableProvider() @@ -998,6 +1074,13 @@ public class SubscriptionUpdatedHandlerTests var newSubscription = new Subscription { Id = previousSubscription?.Id ?? "sub_123", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) } + ] + }, Status = StripeSubscriptionStatus.Active, Metadata = new Dictionary { { "providerId", providerId.ToString() } } }; @@ -1021,7 +1104,10 @@ public class SubscriptionUpdatedHandlerTests { new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid } }, new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Incomplete } }, - new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired } }, + new object[] + { + new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired } + }, new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Paused } } }; } diff --git a/test/Billing.Test/Utilities/StripeTestEvents.cs b/test/Billing.Test/Utilities/StripeTestEvents.cs deleted file mode 100644 index 86792af812..0000000000 --- a/test/Billing.Test/Utilities/StripeTestEvents.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Stripe; - -namespace Bit.Billing.Test.Utilities; - -public enum StripeEventType -{ - ChargeSucceeded, - CustomerSubscriptionUpdated, - CustomerUpdated, - InvoiceCreated, - InvoiceFinalized, - InvoiceUpcoming, - PaymentMethodAttached -} - -public static class StripeTestEvents -{ - public static async Task GetAsync(StripeEventType eventType) - { - var fileName = eventType switch - { - StripeEventType.ChargeSucceeded => "charge.succeeded.json", - StripeEventType.CustomerSubscriptionUpdated => "customer.subscription.updated.json", - StripeEventType.CustomerUpdated => "customer.updated.json", - StripeEventType.InvoiceCreated => "invoice.created.json", - StripeEventType.InvoiceFinalized => "invoice.finalized.json", - StripeEventType.InvoiceUpcoming => "invoice.upcoming.json", - StripeEventType.PaymentMethodAttached => "payment_method.attached.json" - }; - - var resource = await EmbeddedResourceReader.ReadAsync("Events", fileName); - - return EventUtility.ParseEvent(resource); - } -} diff --git a/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs b/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs index a30e5e896c..65d9e99e3b 100644 --- a/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs +++ b/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs @@ -294,7 +294,8 @@ public class InvoiceExtensionsTests Amount = 600 } ); - invoice.Tax = 120; // $1.20 in cents + + invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 120 }]; // $1.20 in cents var subscription = new Subscription(); // Act @@ -318,7 +319,7 @@ public class InvoiceExtensionsTests Amount = 600 } ); - invoice.Tax = null; + invoice.TotalTaxes = []; var subscription = new Subscription(); // Act @@ -341,7 +342,7 @@ public class InvoiceExtensionsTests Amount = 600 } ); - invoice.Tax = 0; + invoice.TotalTaxes = [new InvoiceTotalTax { Amount = 0 }]; var subscription = new Subscription(); // Act @@ -374,7 +375,7 @@ public class InvoiceExtensionsTests var invoice = new Invoice { Lines = lineItems, - Tax = 200 // Additional $2.00 tax + TotalTaxes = [new InvoiceTotalTax { Amount = 200 }] // Additional $2.00 tax }; var subscription = new Subscription(); diff --git a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs index b2e94967ce..04b579add3 100644 --- a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs @@ -227,8 +227,16 @@ If you believe you need to change the version for a valid reason, please discuss Status = "active", TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc), - CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), - CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + Items = new StripeList + { + Data = [ + new SubscriptionItem + { + CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + } + ] + } }; return new SubscriptionInfo diff --git a/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs b/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs index 2d1e21b8c5..90bb619ab4 100644 --- a/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs +++ b/test/Core.Test/Billing/Models/Business/UserLicenseTests.cs @@ -141,8 +141,16 @@ If you believe you need to change the version for a valid reason, please discuss Status = "active", TrialStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), TrialEnd = new DateTime(2024, 2, 1, 0, 0, 0, DateTimeKind.Utc), - CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), - CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + Items = new StripeList + { + Data = [ + new SubscriptionItem + { + CurrentPeriodStart = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + CurrentPeriodEnd = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc) + } + ] + } }; return new SubscriptionInfo diff --git a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs index 08c3d9cf18..8b3a044118 100644 --- a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs @@ -54,7 +54,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 500, + TotalTaxes = [new InvoiceTotalTax { Amount = 500 }], Total = 5500 }; @@ -77,7 +77,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2021-family-for-enterprise-annually" && options.SubscriptionDetails.Items[0].Quantity == 1 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -112,7 +112,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 750, + TotalTaxes = [new InvoiceTotalTax { Amount = 750 }], Total = 8250 }; @@ -137,7 +137,9 @@ public class PreviewOrganizationTaxCommandTests item.Price == "2023-teams-org-seat-monthly" && item.Quantity == 5) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-teams-seat-monthly" && item.Quantity == 3) && - options.Coupon == CouponIDs.SecretsManagerStandalone)); + options.Discounts != null && + options.Discounts.Count == 1 && + options.Discounts[0].Coupon == CouponIDs.SecretsManagerStandalone)); } [Fact] @@ -173,7 +175,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 1200, + TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }], Total = 12200 }; @@ -205,7 +207,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 8) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 3) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -234,7 +236,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 300, + TotalTaxes = [new InvoiceTotalTax { Amount = 300 }], Total = 3300 }; @@ -257,7 +259,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" && options.SubscriptionDetails.Items[0].Quantity == 6 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -286,7 +288,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 0, + TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 2700 }; @@ -309,7 +311,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && options.SubscriptionDetails.Items[0].Quantity == 3 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -339,7 +341,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 2100, + TotalTaxes = [new InvoiceTotalTax { Amount = 2100 }], Total = 12100 }; @@ -365,7 +367,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-enterprise-seat-monthly" && options.SubscriptionDetails.Items[0].Quantity == 15 && - options.Coupon == null)); + options.Discounts == null)); } #endregion @@ -399,7 +401,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 120, + TotalTaxes = [new InvoiceTotalTax { Amount = 120 }], Total = 1320 }; @@ -422,7 +424,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && options.SubscriptionDetails.Items[0].Quantity == 2 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -452,7 +454,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 400, + TotalTaxes = [new InvoiceTotalTax { Amount = 400 }], Total = 4400 }; @@ -475,7 +477,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" && options.SubscriptionDetails.Items[0].Quantity == 1 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -524,7 +526,11 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 900, + TotalTaxes = [new InvoiceTotalTax + { + Amount = 900 + } + ], Total = 9900 }; @@ -546,7 +552,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-annually" && options.SubscriptionDetails.Items[0].Quantity == 6 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -595,7 +601,11 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 1200, + TotalTaxes = [new InvoiceTotalTax + { + Amount = 1200 + } + ], Total = 13200 }; @@ -617,7 +627,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" && options.SubscriptionDetails.Items[0].Quantity == 6 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -647,7 +657,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 800, + TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 8800 }; @@ -672,7 +682,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 2) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 2) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -724,7 +734,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 1500, + TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }], Total = 16500 }; @@ -753,7 +763,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 5) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 10) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -808,7 +818,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 600, + TotalTaxes = [new InvoiceTotalTax { Amount = 600 }], Total = 6600 }; @@ -831,7 +841,9 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-enterprise-org-seat-annually" && options.SubscriptionDetails.Items[0].Quantity == 5 && - options.Coupon == "EXISTING_DISCOUNT_50")); + options.Discounts != null && + options.Discounts.Count == 1 && + options.Discounts[0].Coupon == "EXISTING_DISCOUNT_50")); } [Fact] @@ -911,7 +923,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 600, + TotalTaxes = [new InvoiceTotalTax { Amount = 600 }], Total = 6600 }; @@ -934,7 +946,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && options.SubscriptionDetails.Items[0].Quantity == 10 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -976,7 +988,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 1200, + TotalTaxes = [new InvoiceTotalTax { Amount = 1200 }], Total = 13200 }; @@ -1001,7 +1013,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 15) && options.SubscriptionDetails.Items.Any(item => item.Price == "storage-gb-annually" && item.Quantity == 5) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -1043,7 +1055,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 800, + TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 8800 }; @@ -1066,7 +1078,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "secrets-manager-teams-seat-annually" && options.SubscriptionDetails.Items[0].Quantity == 8 && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -1111,7 +1123,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 1500, + TotalTaxes = [new InvoiceTotalTax { Amount = 1500 }], Total = 16500 }; @@ -1139,7 +1151,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "secrets-manager-enterprise-seat-monthly" && item.Quantity == 12) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-service-account-2024-monthly" && item.Quantity == 20) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -1192,7 +1204,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 2500, + TotalTaxes = [new InvoiceTotalTax { Amount = 2500 }], Total = 27500 }; @@ -1224,7 +1236,9 @@ public class PreviewOrganizationTaxCommandTests item.Price == "secrets-manager-enterprise-seat-annually" && item.Quantity == 15) && options.SubscriptionDetails.Items.Any(item => item.Price == "secrets-manager-service-account-2024-annually" && item.Quantity == 30) && - options.Coupon == "ENTERPRISE_DISCOUNT_20")); + options.Discounts != null && + options.Discounts.Count == 1 && + options.Discounts[0].Coupon == "ENTERPRISE_DISCOUNT_20")); } [Fact] @@ -1266,7 +1280,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 500, + TotalTaxes = [new InvoiceTotalTax { Amount = 500 }], Total = 5500 }; @@ -1291,7 +1305,7 @@ public class PreviewOrganizationTaxCommandTests item.Price == "2020-families-org-annually" && item.Quantity == 6) && options.SubscriptionDetails.Items.Any(item => item.Price == "personal-storage-gb-annually" && item.Quantity == 2) && - options.Coupon == null)); + options.Discounts == null)); } [Fact] @@ -1368,7 +1382,7 @@ public class PreviewOrganizationTaxCommandTests var invoice = new Invoice { - Tax = 300, + TotalTaxes = [new InvoiceTotalTax { Amount = 300 }], Total = 3300 }; @@ -1391,7 +1405,7 @@ public class PreviewOrganizationTaxCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && options.SubscriptionDetails.Items[0].Quantity == 5 && - options.Coupon == null)); + options.Discounts == null)); } #endregion diff --git a/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs index ed3698fb1d..617a136fab 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs @@ -27,25 +27,27 @@ public class GetCloudOrganizationLicenseQueryTests { [Theory] [BitAutoData] - public async Task GetLicenseAsync_InvalidInstallationId_Throws(SutProvider sutProvider, + public async Task GetLicenseAsync_InvalidInstallationId_Throws( + SutProvider sutProvider, Organization organization, Guid installationId, int version) { sutProvider.GetDependency().GetByIdAsync(installationId).ReturnsNull(); - var exception = await Assert.ThrowsAsync( - async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId, version)); + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetLicenseAsync(organization, installationId, version)); Assert.Contains("Invalid installation id", exception.Message); } [Theory] [BitAutoData] - public async Task GetLicenseAsync_DisabledOrganization_Throws(SutProvider sutProvider, + public async Task GetLicenseAsync_DisabledOrganization_Throws( + SutProvider sutProvider, Organization organization, Guid installationId, Installation installation) { installation.Enabled = false; sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); - var exception = await Assert.ThrowsAsync( - async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId)); + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetLicenseAsync(organization, installationId)); Assert.Contains("Invalid installation id", exception.Message); } @@ -71,7 +73,8 @@ public class GetCloudOrganizationLicenseQueryTests [Theory] [BitAutoData] - public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken(SutProvider sutProvider, + public async Task GetLicenseAsync_WhenFeatureFlagEnabled_CreatesToken( + SutProvider sutProvider, Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo, byte[] licenseSignature, string token) { @@ -90,7 +93,8 @@ public class GetCloudOrganizationLicenseQueryTests [Theory] [BitAutoData] - public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider sutProvider, + public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription( + SutProvider sutProvider, Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo, byte[] licenseSignature, Provider provider) { @@ -99,8 +103,17 @@ public class GetCloudOrganizationLicenseQueryTests subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription { - CurrentPeriodStart = DateTime.UtcNow, - CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodStart = DateTime.UtcNow, + CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) + } + ] + } }); installation.Enabled = true; diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 96f9c1496e..05d24bdc34 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -272,7 +272,16 @@ public class GetOrganizationWarningsQueryTests CollectionMethod = CollectionMethod.SendInvoice, Customer = new Customer(), Status = SubscriptionStatus.Active, - CurrentPeriodEnd = now.AddDays(10), + Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = now.AddDays(10) + } + ] + }, TestClock = new TestClock { FrozenTime = now diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs index e808fb10b0..8504d3122a 100644 --- a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Services; @@ -105,6 +106,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -152,6 +163,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -241,7 +262,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; - mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -286,6 +316,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -326,7 +366,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "incomplete"; - mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -342,7 +391,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests // Assert Assert.True(result.IsT0); Assert.True(user.Premium); - Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate); + Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); } [Theory, BitAutoData] @@ -368,7 +417,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; - mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -384,7 +442,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests // Assert Assert.True(result.IsT0); Assert.True(user.Premium); - Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate); + Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); } [Theory, BitAutoData] @@ -411,7 +469,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "active"; // PayPal + active doesn't match pattern - mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); @@ -453,7 +520,16 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var mockSubscription = Substitute.For(); mockSubscription.Id = "sub_123"; mockSubscription.Status = "incomplete"; - mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; var mockInvoice = Substitute.For(); diff --git a/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs index bf7d093dc7..9e919a83f9 100644 --- a/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs @@ -31,7 +31,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 300, + TotalTaxes = [new InvoiceTotalTax { Amount = 300 }], Total = 3300 }; @@ -65,7 +65,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 500, + TotalTaxes = [new InvoiceTotalTax { Amount = 500 }], Total = 5500 }; @@ -101,7 +101,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 250, + TotalTaxes = [new InvoiceTotalTax { Amount = 250 }], Total = 2750 }; @@ -135,7 +135,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 800, + TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 8800 }; @@ -171,7 +171,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 450, + TotalTaxes = [new InvoiceTotalTax { Amount = 450 }], Total = 4950 }; @@ -207,7 +207,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 0, + TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 3000 }; @@ -241,7 +241,7 @@ public class PreviewPremiumTaxCommandTests var invoice = new Invoice { - Tax = 600, + TotalTaxes = [new InvoiceTotalTax { Amount = 600 }], Total = 6600 }; @@ -276,7 +276,7 @@ public class PreviewPremiumTaxCommandTests // Stripe amounts are in cents var invoice = new Invoice { - Tax = 123, // $1.23 + TotalTaxes = [new InvoiceTotalTax { Amount = 123 }], // $1.23 Total = 3123 // $31.23 }; diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 77dce8101c..224328d71b 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -1,10 +1,15 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Organizations.Services; +using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -118,4 +123,232 @@ public class OrganizationBillingServiceTests } #endregion + + #region Finalize - Trial Settings + + [Theory, BitAutoData] + public async Task NoPaymentMethodAndTrialPeriod_SetsMissingPaymentMethodCancelBehavior( + Organization organization, + SutProvider sutProvider) + { + // Arrange + var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + organization.PlanType = PlanType.TeamsAnnually; + organization.GatewayCustomerId = "cus_test123"; + organization.GatewaySubscriptionId = null; + + var subscriptionSetup = new SubscriptionSetup + { + PlanType = PlanType.TeamsAnnually, + PasswordManagerOptions = new SubscriptionSetup.PasswordManager + { + Seats = 5, + Storage = null, + PremiumAccess = false + }, + SecretsManagerOptions = null, + SkipTrial = false + }; + + var sale = new OrganizationSale + { + Organization = organization, + SubscriptionSetup = subscriptionSetup + }; + + sutProvider.GetDependency() + .GetPlanOrThrow(PlanType.TeamsAnnually) + .Returns(plan); + + sutProvider.GetDependency() + .Run(organization) + .Returns(false); + + var customer = new Customer + { + Id = "cus_test123", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow(organization, Arg.Any()) + .Returns(customer); + + SubscriptionCreateOptions capturedOptions = null; + sutProvider.GetDependency() + .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) + .Returns(new Subscription + { + Id = "sub_test123", + Status = StripeConstants.SubscriptionStatus.Trialing + }); + + sutProvider.GetDependency() + .ReplaceAsync(organization) + .Returns(Task.CompletedTask); + + // Act + await sutProvider.Sut.Finalize(sale); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .SubscriptionCreateAsync(Arg.Any()); + + Assert.NotNull(capturedOptions); + Assert.Equal(7, capturedOptions.TrialPeriodDays); + Assert.NotNull(capturedOptions.TrialSettings); + Assert.NotNull(capturedOptions.TrialSettings.EndBehavior); + Assert.Equal("cancel", capturedOptions.TrialSettings.EndBehavior.MissingPaymentMethod); + } + + [Theory, BitAutoData] + public async Task NoPaymentMethodButNoTrial_DoesNotSetMissingPaymentMethodBehavior( + Organization organization, + SutProvider sutProvider) + { + // Arrange + var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + organization.PlanType = PlanType.TeamsAnnually; + organization.GatewayCustomerId = "cus_test123"; + organization.GatewaySubscriptionId = null; + + var subscriptionSetup = new SubscriptionSetup + { + PlanType = PlanType.TeamsAnnually, + PasswordManagerOptions = new SubscriptionSetup.PasswordManager + { + Seats = 5, + Storage = null, + PremiumAccess = false + }, + SecretsManagerOptions = null, + SkipTrial = true // This will result in TrialPeriodDays = 0 + }; + + var sale = new OrganizationSale + { + Organization = organization, + SubscriptionSetup = subscriptionSetup + }; + + sutProvider.GetDependency() + .GetPlanOrThrow(PlanType.TeamsAnnually) + .Returns(plan); + + sutProvider.GetDependency() + .Run(organization) + .Returns(false); + + var customer = new Customer + { + Id = "cus_test123", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow(organization, Arg.Any()) + .Returns(customer); + + SubscriptionCreateOptions capturedOptions = null; + sutProvider.GetDependency() + .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) + .Returns(new Subscription + { + Id = "sub_test123", + Status = StripeConstants.SubscriptionStatus.Active + }); + + sutProvider.GetDependency() + .ReplaceAsync(organization) + .Returns(Task.CompletedTask); + + // Act + await sutProvider.Sut.Finalize(sale); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .SubscriptionCreateAsync(Arg.Any()); + + Assert.NotNull(capturedOptions); + Assert.Equal(0, capturedOptions.TrialPeriodDays); + Assert.Null(capturedOptions.TrialSettings); + } + + [Theory, BitAutoData] + public async Task HasPaymentMethodAndTrialPeriod_DoesNotSetMissingPaymentMethodBehavior( + Organization organization, + SutProvider sutProvider) + { + // Arrange + var plan = StaticStore.GetPlan(PlanType.TeamsAnnually); + organization.PlanType = PlanType.TeamsAnnually; + organization.GatewayCustomerId = "cus_test123"; + organization.GatewaySubscriptionId = null; + + var subscriptionSetup = new SubscriptionSetup + { + PlanType = PlanType.TeamsAnnually, + PasswordManagerOptions = new SubscriptionSetup.PasswordManager + { + Seats = 5, + Storage = null, + PremiumAccess = false + }, + SecretsManagerOptions = null, + SkipTrial = false + }; + + var sale = new OrganizationSale + { + Organization = organization, + SubscriptionSetup = subscriptionSetup + }; + + sutProvider.GetDependency() + .GetPlanOrThrow(PlanType.TeamsAnnually) + .Returns(plan); + + sutProvider.GetDependency() + .Run(organization) + .Returns(true); // Has payment method + + var customer = new Customer + { + Id = "cus_test123", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow(organization, Arg.Any()) + .Returns(customer); + + SubscriptionCreateOptions capturedOptions = null; + sutProvider.GetDependency() + .SubscriptionCreateAsync(Arg.Do(options => capturedOptions = options)) + .Returns(new Subscription + { + Id = "sub_test123", + Status = StripeConstants.SubscriptionStatus.Trialing + }); + + sutProvider.GetDependency() + .ReplaceAsync(organization) + .Returns(Task.CompletedTask); + + // Act + await sutProvider.Sut.Finalize(sale); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .SubscriptionCreateAsync(Arg.Any()); + + Assert.NotNull(capturedOptions); + Assert.Equal(7, capturedOptions.TrialPeriodDays); + Assert.Null(capturedOptions.TrialSettings); + } + + #endregion } diff --git a/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs index a5970c79ab..570f94575f 100644 --- a/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs @@ -88,7 +88,13 @@ public class RestartSubscriptionCommandTests var newSubscription = new Subscription { Id = "sub_new", - CurrentPeriodEnd = currentPeriodEnd + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + } }; _subscriberService.GetSubscription(organization).Returns(existingSubscription); @@ -138,7 +144,13 @@ public class RestartSubscriptionCommandTests var newSubscription = new Subscription { Id = "sub_new", - CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) } + ] + } }; _subscriberService.GetSubscription(provider).Returns(existingSubscription); @@ -177,7 +189,13 @@ public class RestartSubscriptionCommandTests var newSubscription = new Subscription { Id = "sub_new", - CurrentPeriodEnd = currentPeriodEnd + Items = new StripeList + { + Data = + [ + new SubscriptionItem { CurrentPeriodEnd = currentPeriodEnd } + ] + } }; _subscriberService.GetSubscription(user).Returns(existingSubscription); diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 609437b8d1..dd342bd153 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -18,8 +18,9 @@ public class StripePaymentServiceTests { [Theory] [BitAutoData] - public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage( - SutProvider sutProvider) + public async Task + PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage( + SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() @@ -28,16 +29,13 @@ public class StripePaymentServiceTests var parameters = new PreviewOrganizationInvoiceRequestBody { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - AdditionalStorage = 0 - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } + PasswordManager = + new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually, + AdditionalStorage = 0 + }, + TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() @@ -52,7 +50,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 4000, - Tax = 800, + TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 }); @@ -75,16 +73,13 @@ public class StripePaymentServiceTests var parameters = new PreviewOrganizationInvoiceRequestBody { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - AdditionalStorage = 1 - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } + PasswordManager = + new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually, + AdditionalStorage = 1 + }, + TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() @@ -96,12 +91,7 @@ public class StripePaymentServiceTests p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 1))) - .Returns(new Invoice - { - TotalExcludingTax = 4000, - Tax = 800, - Total = 4800 - }); + .Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); @@ -112,8 +102,9 @@ public class StripePaymentServiceTests [Theory] [BitAutoData] - public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage( - SutProvider sutProvider) + public async Task + PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage( + SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() @@ -128,11 +119,7 @@ public class StripePaymentServiceTests SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, AdditionalStorage = 0 }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } + TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() @@ -144,12 +131,7 @@ public class StripePaymentServiceTests p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 0))) - .Returns(new Invoice - { - TotalExcludingTax = 0, - Tax = 0, - Total = 0 - }); + .Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); @@ -160,8 +142,9 @@ public class StripePaymentServiceTests [Theory] [BitAutoData] - public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage( - SutProvider sutProvider) + public async Task + PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage( + SutProvider sutProvider) { var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() @@ -176,11 +159,7 @@ public class StripePaymentServiceTests SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, AdditionalStorage = 1 }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } + TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } }; sutProvider.GetDependency() @@ -192,12 +171,7 @@ public class StripePaymentServiceTests p.SubscriptionDetails.Items.Any(x => x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && x.Quantity == 1))) - .Returns(new Invoice - { - TotalExcludingTax = 400, - Tax = 8, - Total = 408 - }); + .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); @@ -235,7 +209,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -277,7 +251,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -319,7 +293,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -361,7 +335,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -403,7 +377,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -445,7 +419,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -487,7 +461,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); @@ -529,7 +503,7 @@ public class StripePaymentServiceTests .Returns(new Invoice { TotalExcludingTax = 400, - Tax = 8, + TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); From 44a82d3b226e352afd1ea610d9bdab5288af8c9e Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 21 Oct 2025 23:46:37 +0200 Subject: [PATCH 38/52] [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. --- .github/CODEOWNERS | 3 + .github/renovate.json5 | 6 + .github/workflows/test.yml | 8 + .gitignore | 2 + bitwarden-server.sln | 6 + util/RustSdk/NativeMethods.cs | 58 + util/RustSdk/RustSdk.csproj | 41 + util/RustSdk/RustSdkException.cs | 19 + util/RustSdk/RustSdkService.cs | 104 + util/RustSdk/RustSdkServiceFactory.cs | 21 + util/RustSdk/rust/Cargo.lock | 3147 +++++++++++++++++++ util/RustSdk/rust/Cargo.toml | 33 + util/RustSdk/rust/build.rs | 9 + util/RustSdk/rust/src/lib.rs | 152 + util/Seeder/Factories/OrganizationSeeder.cs | 14 + util/Seeder/Factories/UserSeeder.cs | 27 + util/Seeder/Seeder.csproj | 1 + 17 files changed, 3651 insertions(+) create mode 100644 util/RustSdk/NativeMethods.cs create mode 100644 util/RustSdk/RustSdk.csproj create mode 100644 util/RustSdk/RustSdkException.cs create mode 100644 util/RustSdk/RustSdkService.cs create mode 100644 util/RustSdk/RustSdkServiceFactory.cs create mode 100644 util/RustSdk/rust/Cargo.lock create mode 100644 util/RustSdk/rust/Cargo.toml create mode 100644 util/RustSdk/rust/build.rs create mode 100644 util/RustSdk/rust/src/lib.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6db4905fec..44c7cfdf8c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -96,6 +96,9 @@ 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 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5c01832c06..5cf7aa29aa 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -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,6 +10,11 @@ "nuget", ], packageRules: [ + { + groupName: "cargo minor", + matchManagers: ["cargo"], + matchUpdateTypes: ["minor"], + }, { groupName: "dockerfile minor", matchManagers: ["dockerfile"], diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d584d6c0af..7783fa14b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,14 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + - name: Install rust + uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # stable + with: + toolchain: stable + + - name: Cache cargo registry + uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7 + - name: Print environment run: | dotnet --info diff --git a/.gitignore b/.gitignore index 14ab10a18e..5a708ede30 100644 --- a/.gitignore +++ b/.gitignore @@ -216,6 +216,8 @@ bitwarden_license/src/Sso/wwwroot/assets .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 diff --git a/bitwarden-server.sln b/bitwarden-server.sln index dbc37372a1..d2fc61166e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -133,6 +133,7 @@ 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}" EndProject Global @@ -339,6 +340,10 @@ 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 @@ -397,6 +402,7 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/util/RustSdk/NativeMethods.cs b/util/RustSdk/NativeMethods.cs new file mode 100644 index 0000000000..a118517929 --- /dev/null +++ b/util/RustSdk/NativeMethods.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Bit.RustSDK; + +public static partial class NativeMethods +{ + // https://docs.microsoft.com/en-us/dotnet/standard/native-interop/cross-platform + // Library path will search + // win => __DllName, __DllName.dll + // linux, osx => __DllName.so, __DllName.dylib + + static NativeMethods() + { + NativeLibrary.SetDllImportResolver(typeof(NativeMethods).Assembly, DllImportResolver); + } + + static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + if (libraryName != __DllName) return IntPtr.Zero; + + var path = "runtimes/"; + var extension = ""; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + path += "win-"; + extension = ".dll"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + path += "osx-"; + extension = ".dylib"; + } + else + { + path += "linux-"; + extension = ".so"; + } + + if (RuntimeInformation.ProcessArchitecture == Architecture.X86) + { + path += "x86"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.X64) + { + path += "x64"; + } + else if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + { + path += "arm64"; + } + + path += "/native/" + __DllName + extension; + + return NativeLibrary.Load(Path.Combine(AppContext.BaseDirectory, path), assembly, searchPath); + } +} diff --git a/util/RustSdk/RustSdk.csproj b/util/RustSdk/RustSdk.csproj new file mode 100644 index 0000000000..ba16a55661 --- /dev/null +++ b/util/RustSdk/RustSdk.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + enable + Bit.RustSDK + Bit.RustSDK + true + + + + + Always + true + runtimes/osx-arm64/native/libsdk.dylib + + + Always + true + runtimes/linux-x64/native/libsdk.dylib + + + Always + true + runtimes/windows-x64/native/libsdk.dylib + + + + + + + + + + + + + + diff --git a/util/RustSdk/RustSdkException.cs b/util/RustSdk/RustSdkException.cs new file mode 100644 index 0000000000..52cbc3635f --- /dev/null +++ b/util/RustSdk/RustSdkException.cs @@ -0,0 +1,19 @@ +namespace Bit.RustSDK; + +/// +/// Exception thrown when the Rust SDK operations fail +/// +public class RustSdkException : Exception +{ + public RustSdkException() : base("An error occurred in the Rust SDK operation") + { + } + + public RustSdkException(string message) : base(message) + { + } + + public RustSdkException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/util/RustSdk/RustSdkService.cs b/util/RustSdk/RustSdkService.cs new file mode 100644 index 0000000000..ee01d56fee --- /dev/null +++ b/util/RustSdk/RustSdkService.cs @@ -0,0 +1,104 @@ +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; + +namespace Bit.RustSDK; + +public class UserKeys +{ + public required string MasterPasswordHash { get; set; } + /// + /// Base64 encoded UserKey + /// + public required string Key { get; set; } + public required string EncryptedUserKey { get; set; } + public required string PublicKey { get; set; } + public required string PrivateKey { get; set; } +} + +public class OrganizationKeys +{ + /// + /// Base64 encoded SymmetricCryptoKey + /// + public required string Key { get; set; } + + public required string PublicKey { get; set; } + public required string PrivateKey { get; set; } +} + +/// +/// Service implementation that provides a C# friendly interface to the Rust SDK +/// +public class RustSdkService +{ + private static readonly JsonSerializerOptions CaseInsensitiveOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public unsafe UserKeys GenerateUserKeys(string email, string password) + { + var emailBytes = StringToRustString(email); + var passwordBytes = StringToRustString(password); + + fixed (byte* emailPtr = emailBytes) + fixed (byte* passwordPtr = passwordBytes) + { + var resultPtr = NativeMethods.generate_user_keys(emailPtr, passwordPtr); + + var result = TakeAndDestroyRustString(resultPtr); + + return JsonSerializer.Deserialize(result, CaseInsensitiveOptions)!; + } + } + + public unsafe OrganizationKeys GenerateOrganizationKeys() + { + var resultPtr = NativeMethods.generate_organization_keys(); + + var result = TakeAndDestroyRustString(resultPtr); + + return JsonSerializer.Deserialize(result, CaseInsensitiveOptions)!; + } + + public unsafe string GenerateUserOrganizationKey(string userKey, string orgKey) + { + var userKeyBytes = StringToRustString(userKey); + var orgKeyBytes = StringToRustString(orgKey); + + fixed (byte* userKeyPtr = userKeyBytes) + fixed (byte* orgKeyPtr = orgKeyBytes) + { + var resultPtr = NativeMethods.generate_user_organization_key(userKeyPtr, orgKeyPtr); + + var result = TakeAndDestroyRustString(resultPtr); + + return result; + } + } + + + private static byte[] StringToRustString(string str) + { + return Encoding.UTF8.GetBytes(str + '\0'); + } + + private static unsafe string TakeAndDestroyRustString(byte* ptr) + { + if (ptr == null) + { + throw new RustSdkException("Pointer is null"); + } + + var result = Marshal.PtrToStringUTF8((IntPtr)ptr); + NativeMethods.free_c_string(ptr); + + if (result == null) + { + throw new RustSdkException("Failed to convert native result to string"); + } + + return result; + } +} diff --git a/util/RustSdk/RustSdkServiceFactory.cs b/util/RustSdk/RustSdkServiceFactory.cs new file mode 100644 index 0000000000..e19275aacb --- /dev/null +++ b/util/RustSdk/RustSdkServiceFactory.cs @@ -0,0 +1,21 @@ +namespace Bit.RustSDK; + +/// +/// Factory for creating Rust SDK service instances +/// +public static class RustSdkServiceFactory +{ + /// + /// Creates a singleton instance of the Rust SDK service (thread-safe) + /// + /// A singleton IRustSdkService instance + public static RustSdkService CreateSingleton() + { + return SingletonHolder.Instance; + } + + private static class SingletonHolder + { + internal static readonly RustSdkService Instance = new(); + } +} diff --git a/util/RustSdk/rust/Cargo.lock b/util/RustSdk/rust/Cargo.lock new file mode 100644 index 0000000000..cba06d35ea --- /dev/null +++ b/util/RustSdk/rust/Cargo.lock @@ -0,0 +1,3147 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bitwarden-api-api" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "async-trait", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + +[[package]] +name = "bitwarden-api-identity" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "async-trait", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + +[[package]] +name = "bitwarden-core" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "async-trait", + "bitwarden-api-api", + "bitwarden-api-identity", + "bitwarden-crypto", + "bitwarden-encoding", + "bitwarden-error", + "bitwarden-state", + "bitwarden-uuid", + "chrono", + "getrandom 0.2.16", + "log", + "rand 0.8.5", + "reqwest", + "rustls", + "rustls-platform-verifier", + "schemars 1.0.4", + "serde", + "serde_bytes", + "serde_json", + "serde_qs", + "serde_repr", + "thiserror 1.0.69", + "uuid", + "zeroize", +] + +[[package]] +name = "bitwarden-crypto" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "aes", + "argon2", + "bitwarden-encoding", + "bitwarden-error", + "cbc", + "chacha20poly1305", + "ciborium", + "coset", + "ed25519-dalek", + "generic-array", + "hkdf", + "hmac", + "num-bigint", + "num-traits", + "pbkdf2", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "rsa", + "schemars 1.0.4", + "serde", + "serde_bytes", + "serde_repr", + "sha1", + "sha2", + "subtle", + "thiserror 1.0.69", + "typenum", + "uuid", + "zeroize", + "zeroizing-alloc", +] + +[[package]] +name = "bitwarden-encoding" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "data-encoding", + "data-encoding-macro", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "bitwarden-error" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "bitwarden-error-macro", +] + +[[package]] +name = "bitwarden-error-macro" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitwarden-state" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "async-trait", + "bitwarden-error", + "bitwarden-threading", + "indexed-db", + "js-sys", + "rusqlite", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tsify", +] + +[[package]] +name = "bitwarden-threading" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "bitwarden-error", + "log", + "serde", + "thiserror 1.0.69", + "tokio", + "tokio-util", +] + +[[package]] +name = "bitwarden-uuid" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "bitwarden-uuid-macro", +] + +[[package]] +name = "bitwarden-uuid-macro" +version = "1.0.0" +source = "git+https://github.com/bitwarden/sdk-internal.git?rev=1461b3ba6bb6e2d0114770eb4572a1398b4789ef#1461b3ba6bb6e2d0114770eb4572a1398b4789ef" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "coset" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" +dependencies = [ + "ciborium", + "ciborium-io", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csbindgen" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26b9831049b947d154bba920e4124053def72447be6fb106a96f483874b482a" +dependencies = [ + "regex", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "data-encoding-macro" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ce6c96ea0102f01122a185683611bd5ac8d99e62bc59dd12e6bda344ee673d" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.4", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexed-db" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f4ecbb6cd50773303683617a93fc2782267d2c94546e9545ec4190eb69aa1a" +dependencies = [ + "futures-channel", + "futures-util", + "pin-project-lite", + "scoped-tls", + "thiserror 2.0.12", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda84358ed17f1f354cf4b1909ad346e6c7bc2513e8c40eb08e0157aa13a9070" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "chrono", + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sdk" +version = "0.1.0" +dependencies = [ + "base64", + "bitwarden-core", + "bitwarden-crypto", + "csbindgen", + "serde", + "serde_json", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.10.0", + "schemars 0.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ec91b85e6c6592ed28636cb1dd1fac377ecbbeb170ff1d79f97aac5e38926d" +dependencies = [ + "serde", + "serde-wasm-bindgen", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a324606929ad11628a19206d7853807481dcaecd6c08be70a235930b8241955" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroizing-alloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebff5e6b81c1c7dca2d0bd333b2006da48cb37dbcae5a8da888f31fcb3c19934" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/util/RustSdk/rust/Cargo.toml b/util/RustSdk/rust/Cargo.toml new file mode 100644 index 0000000000..88521277f3 --- /dev/null +++ b/util/RustSdk/rust/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "sdk" +publish = false + +version = "0.1.0" +authors = ["Bitwarden Inc"] +edition = "2021" +homepage = "https://bitwarden.com" +repository = "https://github.com/bitwarden/server" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +base64 = "0.22.1" +bitwarden-core = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" } +bitwarden-crypto = { git = "https://github.com/bitwarden/sdk-internal.git", rev = "1461b3ba6bb6e2d0114770eb4572a1398b4789ef" } +serde = "=1.0.219" +serde_json = "=1.0.141" + +[build-dependencies] +csbindgen = "=1.9.3" + +# Compile all dependencies with some optimizations when building this crate on debug +# This slows down clean builds by about 50%, but the resulting binaries can be orders of magnitude faster +# As clean builds won't occur very often, this won't slow down the development process +[profile.dev.package."*"] +opt-level = 2 + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 3 diff --git a/util/RustSdk/rust/build.rs b/util/RustSdk/rust/build.rs new file mode 100644 index 0000000000..0905afc22d --- /dev/null +++ b/util/RustSdk/rust/build.rs @@ -0,0 +1,9 @@ +fn main() { + csbindgen::Builder::default() + .input_extern_file("src/lib.rs") + .csharp_dll_name("libsdk") + .csharp_namespace("Bit.RustSDK") + .csharp_class_accessibility("public") + .generate_csharp_file("../NativeMethods.g.cs") + .unwrap(); +} diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs new file mode 100644 index 0000000000..10f8d8dca4 --- /dev/null +++ b/util/RustSdk/rust/src/lib.rs @@ -0,0 +1,152 @@ +#![allow(clippy::missing_safety_doc)] +use std::{ + ffi::{c_char, CStr, CString}, + num::NonZeroU32, +}; + +use base64::{engine::general_purpose::STANDARD, Engine}; + +use bitwarden_crypto::{ + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, Kdf, + KeyEncryptable, MasterKey, RsaKeyPair, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, +}; + +#[no_mangle] +pub unsafe extern "C" fn generate_user_keys( + email: *const c_char, + password: *const c_char, +) -> *const c_char { + let email = CStr::from_ptr(email).to_str().unwrap(); + let password = CStr::from_ptr(password).to_str().unwrap(); + + println!("Generating keys for {email}"); + println!("Password: {password}"); + + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(5_000).unwrap(), + }; + + let master_key = MasterKey::derive(password, email, &kdf).unwrap(); + + let master_password_hash = + master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization); + + println!("Master password hash: {}", master_password_hash); + + let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap(); + + let keypair = keypair(&user_key.0); + + let json = serde_json::json!({ + "masterPasswordHash": master_password_hash, + "key": user_key.0.to_base64(), + "encryptedUserKey": encrypted_user_key.to_string(), + "publicKey": keypair.public.to_string(), + "privateKey": keypair.private.to_string(), + }) + .to_string(); + + let result = CString::new(json).unwrap(); + + result.into_raw() +} + +fn keypair(key: &SymmetricCryptoKey) -> RsaKeyPair { + const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS +8HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2 +e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9 +4LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa +F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A +QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16 +l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq +92qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP +dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a +PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX +UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR +buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2 +hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc +vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA +hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs +tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY +UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs +zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX +E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP +jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez +MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9 +JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg +AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp +Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3 +WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz +XKZBokBGnjFnTnKcs7nv/O8= +-----END PRIVATE KEY-----"; + + let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap(); + let public_key = private_key.to_public_key().to_der().unwrap(); + + let p = private_key.to_der().unwrap(); + + RsaKeyPair { + private: p.encrypt_with_key(key).unwrap(), + public: public_key.into(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn generate_organization_keys() -> *const c_char { + let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + + let key = UserKey::new(key); + let keypair = key.make_key_pair().expect("Failed to generate key pair"); + + let json = serde_json::json!({ + "key": key.0.to_base64(), + "publicKey": keypair.public.to_string(), + "privateKey": keypair.private.to_string(), + }) + .to_string(); + + let result = CString::new(json).unwrap(); + + result.into_raw() +} + +#[no_mangle] +pub unsafe extern "C" fn generate_user_organization_key( + user_public_key: *const c_char, + organization_key: *const c_char, +) -> *const c_char { + let user_public_key = CStr::from_ptr(user_public_key).to_str().unwrap().to_owned(); + let organization_key = CStr::from_ptr(organization_key) + .to_str() + .unwrap() + .to_owned(); + + let user_public_key = STANDARD.decode(user_public_key).unwrap(); + let organization_key = STANDARD.decode(organization_key).unwrap(); + + let encapsulation_key = + AsymmetricPublicCryptoKey::from_der(&SpkiPublicKeyBytes::from(user_public_key)).unwrap(); + + let encrypted_key = UnsignedSharedKey::encapsulate_key_unsigned( + &SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(organization_key)).unwrap(), + &encapsulation_key, + ) + .unwrap(); + + let result = CString::new(encrypted_key.to_string()).unwrap(); + + result.into_raw() +} + +/// # Safety +/// +/// The `str` pointer must be a valid pointer previously returned by `CString::into_raw` +/// and must not have already been freed. After calling this function, the pointer must not be used again. +#[no_mangle] +pub unsafe extern "C" fn free_c_string(str: *mut c_char) { + unsafe { + drop(CString::from_raw(str)); + } +} diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs index 5e5cb17419..f6f05d9525 100644 --- a/util/Seeder/Factories/OrganizationSeeder.cs +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -41,4 +41,18 @@ public static class OrgnaizationExtensions Status = OrganizationUserStatusType.Confirmed }; } + + public static OrganizationUser CreateSdkOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } } diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs index 90cadf0b78..b24f8273b9 100644 --- a/util/Seeder/Factories/UserSeeder.cs +++ b/util/Seeder/Factories/UserSeeder.cs @@ -1,5 +1,7 @@ using Bit.Core.Enums; using Bit.Infrastructure.EntityFramework.Models; +using Bit.RustSDK; +using Microsoft.AspNetCore.Identity; namespace Bit.Seeder.Factories; @@ -22,4 +24,29 @@ public class UserSeeder KdfIterations = 600_000, }; } + + public static (User user, string userKey) CreateSdkUser(IPasswordHasher passwordHasher, string email) + { + var nativeService = RustSdkServiceFactory.CreateSingleton(); + var keys = nativeService.GenerateUserKeys(email, "asdfasdfasdf"); + + var user = new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = null, + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = keys.EncryptedUserKey, + PublicKey = keys.PublicKey, + PrivateKey = keys.PrivateKey, + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 5_000, + }; + + user.MasterPassword = passwordHasher.HashPassword(user, keys.MasterPasswordHash); + + return (user, keys.Key); + } } diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj index 392f6434cc..4d7fbab767 100644 --- a/util/Seeder/Seeder.csproj +++ b/util/Seeder/Seeder.csproj @@ -20,6 +20,7 @@ + From 0a205722b4f535a6a38b9e63a1da8dd9f21dacdd Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 22 Oct 2025 01:11:40 +0200 Subject: [PATCH 39/52] Add SSHAgentV2 constant to Constants.cs (#6461) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 54e8b07400..da7b94aada 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -161,6 +161,7 @@ public static class FeatureFlagKeys public const string InlineMenuFieldQualification = "inline-menu-field-qualification"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string SSHAgent = "ssh-agent"; + public const string SSHAgentV2 = "ssh-agent-v2"; public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override"; public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor"; public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2"; From 3866bc5155136c7212667e766d6019ee07b6c2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:20:53 +0100 Subject: [PATCH 40/52] =?UTF-8?q?[PM-23134]=C2=A0Update=20PolicyDetails=20?= =?UTF-8?q?sprocs=20for=20performance=20(#6421)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../Implementations/PolicyRequirementQuery.cs | 10 +- .../Repositories/IPolicyRepository.cs | 11 - .../Repositories/PolicyRepository.cs | 13 - .../Repositories/PolicyRepository.cs | 39 -- ...tionUser_ReadByUserIdWithPolicyDetails.sql | 91 +++- src/Sql/dbo/Views/UserProviderAccessView.sql | 9 + .../Policies/PolicyRequirementQueryTests.cs | 20 +- .../GetByUserIdWithPolicyDetailsTests.cs | 447 ++++++++++++++++++ .../GetPolicyDetailsByUserIdTests.cs | 385 --------------- ...-10-15_00_RefactorPolicyDetailsQueries.sql | 85 ++++ 10 files changed, 623 insertions(+), 487 deletions(-) create mode 100644 src/Sql/dbo/Views/UserProviderAccessView.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs delete mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs create mode 100644 util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index e846e02e46..c1450c6ab5 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; @@ -20,7 +18,7 @@ public class PolicyRequirementQuery( throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); } - var policyDetails = await GetPolicyDetails(userId); + var policyDetails = await GetPolicyDetails(userId, factory.PolicyType); var filteredPolicies = policyDetails .Where(p => p.PolicyType == factory.PolicyType) .Where(factory.Enforce); @@ -48,8 +46,8 @@ public class PolicyRequirementQuery( return eligibleOrganizationUserIds; } - private Task> GetPolicyDetails(Guid userId) - => policyRepository.GetPolicyDetailsByUserId(userId); + private async Task> GetPolicyDetails(Guid userId, PolicyType policyType) + => await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType([userId], policyType); private async Task> GetOrganizationPolicyDetails(Guid organizationId, PolicyType policyType) => await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType); diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 9f5c7f3fc4..d479809b89 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -20,17 +20,6 @@ public interface IPolicyRepository : IRepository Task GetByOrganizationIdTypeAsync(Guid organizationId, PolicyType type); Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdAsync(Guid userId); - /// - /// Gets all PolicyDetails for a user for all policy types. - /// - /// - /// Each PolicyDetail represents an OrganizationUser and a Policy which *may* be enforced - /// against them. It only returns PolicyDetails for policies that are enabled and where the organization's plan - /// supports policies. It also excludes "revoked invited" users who are not subject to policy enforcement. - /// This is consumed by to create requirements for specific policy types. - /// You probably do not want to call it directly. - /// - Task> GetPolicyDetailsByUserId(Guid userId); /// /// Retrieves of the specified diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 83d5ef6a70..865c4f8e5c 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -61,19 +61,6 @@ public class PolicyRepository : Repository, IPolicyRepository } } - public async Task> GetPolicyDetailsByUserId(Guid userId) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[PolicyDetails_ReadByUserId]", - new { UserId = userId }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - public async Task> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable userIds, PolicyType type) { await using var connection = new SqlConnection(ConnectionString); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 72c277f1d7..1cca7a9bbb 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -56,45 +56,6 @@ public class PolicyRepository : Repository> GetPolicyDetailsByUserId(Guid userId) - { - using var scope = ServiceScopeFactory.CreateScope(); - var dbContext = GetDatabaseContext(scope); - - var providerOrganizations = from pu in dbContext.ProviderUsers - where pu.UserId == userId - join po in dbContext.ProviderOrganizations - on pu.ProviderId equals po.ProviderId - select po; - - var query = from p in dbContext.Policies - join ou in dbContext.OrganizationUsers - on p.OrganizationId equals ou.OrganizationId - join o in dbContext.Organizations - on p.OrganizationId equals o.Id - where - p.Enabled && - o.Enabled && - o.UsePolicies && - ( - (ou.Status != OrganizationUserStatusType.Invited && ou.UserId == userId) || - // Invited orgUsers do not have a UserId associated with them, so we have to match up their email - (ou.Status == OrganizationUserStatusType.Invited && ou.Email == dbContext.Users.Find(userId).Email) - ) - select new PolicyDetails - { - OrganizationUserId = ou.Id, - OrganizationId = p.OrganizationId, - PolicyType = p.Type, - PolicyData = p.Data, - OrganizationUserType = ou.Type, - OrganizationUserStatus = ou.Status, - OrganizationUserPermissionsData = ou.Permissions, - IsProvider = providerOrganizations.Any(po => po.OrganizationId == p.OrganizationId) - }; - return await query.ToListAsync(); - } - public async Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql index c2bc690a27..105170cd27 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIdWithPolicyDetails.sql @@ -4,31 +4,70 @@ AS BEGIN SET NOCOUNT ON -SELECT - OU.[Id] AS OrganizationUserId, - P.[OrganizationId], - P.[Type] AS PolicyType, - P.[Enabled] AS PolicyEnabled, - P.[Data] AS PolicyData, - OU.[Type] AS OrganizationUserType, - OU.[Status] AS OrganizationUserStatus, - OU.[Permissions] AS OrganizationUserPermissionsData, - CASE WHEN EXISTS ( - SELECT 1 - FROM [dbo].[ProviderUserView] PU - INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] - WHERE PU.[UserId] = OU.[UserId] AND PO.[OrganizationId] = P.[OrganizationId] - ) THEN 1 ELSE 0 END AS IsProvider -FROM [dbo].[PolicyView] P -INNER JOIN [dbo].[OrganizationUserView] OU - ON P.[OrganizationId] = OU.[OrganizationId] -WHERE P.[Type] = @PolicyType AND + + DECLARE @UserEmail NVARCHAR(256) + SELECT @UserEmail = Email + FROM + [dbo].[UserView] + WHERE + Id = @UserId + + ;WITH OrgUsers AS ( - (OU.[Status] != 0 AND OU.[UserId] = @UserId) -- OrgUsers who have accepted their invite and are linked to a UserId - OR EXISTS ( - SELECT 1 - FROM [dbo].[UserView] U - WHERE U.[Id] = @UserId AND OU.[Email] = U.[Email] AND OU.[Status] = 0 -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email - ) + -- All users except invited (Status <> 0): direct UserId match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] <> 0 + AND OU.[UserId] = @UserId + + UNION ALL + + -- Invited users: email match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] = 0 + AND OU.[Email] = @UserEmail + AND @UserEmail IS NOT NULL + ), + Providers AS + ( + SELECT + OrganizationId + FROM + [dbo].[UserProviderAccessView] + WHERE + UserId = @UserId ) -END \ No newline at end of file + SELECT + OU.[Id] AS [OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Enabled] AS [PolicyEnabled], + P.[Data] AS [PolicyData], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData], + CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider] + FROM + [dbo].[PolicyView] P + INNER JOIN + OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + Providers PR ON PR.[OrganizationId] = OU.[OrganizationId] + WHERE + P.[Type] = @PolicyType +END diff --git a/src/Sql/dbo/Views/UserProviderAccessView.sql b/src/Sql/dbo/Views/UserProviderAccessView.sql new file mode 100644 index 0000000000..dedc380311 --- /dev/null +++ b/src/Sql/dbo/Views/UserProviderAccessView.sql @@ -0,0 +1,9 @@ +CREATE VIEW [dbo].[UserProviderAccessView] +AS +SELECT DISTINCT + PU.[UserId], + PO.[OrganizationId] +FROM + [dbo].[ProviderUserView] PU +INNER JOIN + [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index 8c25f70454..9115ae5ba1 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -14,10 +14,12 @@ public class PolicyRequirementQueryTests [Theory, BitAutoData] public async Task GetAsync_IgnoresOtherPolicyTypes(Guid userId) { - var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - var otherPolicy = new PolicyDetails { PolicyType = PolicyType.RequireSso }; + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = userId }; var policyRepository = Substitute.For(); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([otherPolicy, thisPolicy]); + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([otherPolicy, thisPolicy]); var factory = new TestPolicyRequirementFactory(_ => true); var sut = new PolicyRequirementQuery(policyRepository, [factory]); @@ -33,9 +35,11 @@ public class PolicyRequirementQueryTests { // Arrange policies var policyRepository = Substitute.For(); - var thisPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - var otherPolicy = new PolicyDetails { PolicyType = PolicyType.SingleOrg }; - policyRepository.GetPolicyDetailsByUserId(userId).Returns([thisPolicy, otherPolicy]); + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = userId }; + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([thisPolicy, otherPolicy]); // Arrange a substitute Enforce function so that we can inspect the received calls var callback = Substitute.For>(); @@ -70,7 +74,9 @@ public class PolicyRequirementQueryTests public async Task GetAsync_HandlesNoPolicies(Guid userId) { var policyRepository = Substitute.For(); - policyRepository.GetPolicyDetailsByUserId(userId).Returns([]); + policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + Arg.Is>(ids => ids.Contains(userId)), PolicyType.SingleOrg) + .Returns([]); var factory = new TestPolicyRequirementFactory(x => x.IsProvider); var sut = new PolicyRequirementQuery(policyRepository, [factory]); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs new file mode 100644 index 0000000000..a9ec374a94 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetByUserIdWithPolicyDetailsTests.cs @@ -0,0 +1,447 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; + +public class GetByUserIdWithPolicyDetailsTests +{ + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithConfirmedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + Data = CoreHelpers.ClassToJsonData(new { Setting = "value" }) + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.Equal(org.Id, policyDetails.OrganizationId); + Assert.Equal(PolicyType.SingleOrg, policyDetails.PolicyType); + Assert.True(policyDetails.PolicyEnabled); + Assert.Equal(OrganizationUserType.User, policyDetails.OrganizationUserType); + Assert.Equal(OrganizationUserStatusType.Confirmed, policyDetails.OrganizationUserStatus); + Assert.False(policyDetails.IsProvider); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithAcceptedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.Admin, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = false, // Note: disabled policy + Type = PolicyType.RequireSso, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.False(policyDetails.PolicyEnabled); // Should return even if disabled + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithInvitedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = null, // invited users have null userId + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, + Email = user.Email // invited users have matching Email + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.TwoFactorAuthentication, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.TwoFactorAuthentication); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(orgUser.Id, policyDetails.OrganizationUserId); + Assert.Equal(OrganizationUserStatusType.Invited, policyDetails.OrganizationUserStatus); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithRevokedUser_ReturnsPolicy( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Revoked, + Type = OrganizationUserType.Owner, + Email = null + }; + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.Equal(OrganizationUserStatusType.Revoked, policyDetails.OrganizationUserStatus); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithMultipleOrganizations_ReturnsAllMatchingPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + // Org1 with SingleOrg policy + var org1 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 1", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser1 = new OrganizationUser + { + OrganizationId = org1.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + }; + await organizationUserRepository.CreateAsync(orgUser1); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org1.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org2 with SingleOrg policy + var org2 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 2", + BillingEmail = "billing2@example.com", + Plan = "Test", + }); + var orgUser2 = new OrganizationUser + { + OrganizationId = org2.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Admin, + }; + await organizationUserRepository.CreateAsync(orgUser2); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org2.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Org3 with RequireSso policy (different type - should not be returned) + var org3 = await organizationRepository.CreateAsync(new Organization + { + Name = "Org 3", + BillingEmail = "billing3@example.com", + Plan = "Test", + }); + var orgUser3 = new OrganizationUser + { + OrganizationId = org3.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }; + await organizationUserRepository.CreateAsync(orgUser3); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org3.Id, + Enabled = true, + Type = PolicyType.RequireSso, + }); + + // Act + var result = (await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg)).ToList(); + + // Assert - should only get 2 policies (org1 and org2) + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.OrganizationId == org1.Id && p.OrganizationUserType == OrganizationUserType.User); + Assert.Contains(result, p => p.OrganizationId == org2.Id && p.OrganizationUserType == OrganizationUserType.Admin); + Assert.DoesNotContain(result, p => p.OrganizationId == org3.Id); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithNonExistingPolicyType_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.RequireSso); + + // Assert + Assert.Empty(result); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithProviderUser_ReturnsIsProviderTrue( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + var provider = await providerRepository.CreateAsync(new Provider + { + Name = Guid.NewGuid().ToString(), + Enabled = true + }); + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed + }); + await providerOrganizationRepository.CreateAsync(new ProviderOrganization + { + OrganizationId = org.Id, + ProviderId = provider.Id + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.True(policyDetails.IsProvider); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WithCustomUserWithPermissions_ReturnsPermissions( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + var orgUser = new OrganizationUser + { + OrganizationId = org.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Custom, + Email = null + }; + orgUser.SetPermissions(new Permissions + { + ManagePolicies = true, + EditAnyCollection = true + }); + await organizationUserRepository.CreateAsync(orgUser); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + var policyDetails = result.Single(); + Assert.NotNull(policyDetails.OrganizationUserPermissionsData); + var permissions = CoreHelpers.LoadClassFromJsonData(policyDetails.OrganizationUserPermissionsData); + Assert.True(permissions.ManagePolicies); + Assert.True(permissions.EditAnyCollection); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WhenNoPolicyExists_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + Assert.Empty(result); + } + + [Theory, DatabaseData] + public async Task GetByUserIdWithPolicyDetailsAsync_WhenUserNotInOrg_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var org = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = "billing@example.com", + Plan = "Test", + }); + await policyRepository.CreateAsync(new Policy + { + OrganizationId = org.Id, + Enabled = true, + Type = PolicyType.SingleOrg, + }); + + // Act + var result = await organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(user.Id, PolicyType.SingleOrg); + + // Assert + Assert.Empty(result); + } +} + diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs deleted file mode 100644 index 0a2ddd7387..0000000000 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs +++ /dev/null @@ -1,385 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Enums; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Repositories; -using Bit.Core.Utilities; -using Xunit; - -namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; - -public class GetPolicyDetailsByUserIdTests -{ - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - // OrgUser1 - owner of org1 - confirmed - var user = await userRepository.CreateTestUserAsync(); - var org1 = await CreateEnterpriseOrg(organizationRepository); - var orgUser1 = new OrganizationUser - { - OrganizationId = org1.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Owner, - Email = null // confirmed OrgUsers use the email on the User table - }; - await organizationUserRepository.CreateAsync(orgUser1); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org1.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }) - }); - - // OrgUser2 - custom user of org2 - accepted - var org2 = await CreateEnterpriseOrg(organizationRepository); - var orgUser2 = new OrganizationUser - { - OrganizationId = org2.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.Custom, - Email = null // accepted OrgUsers use the email on the User table - }; - orgUser2.SetPermissions(new Permissions - { - ManagePolicies = true - }); - await organizationUserRepository.CreateAsync(orgUser2); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org2.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - Data = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }) - }); - - // Act - var policyDetails = (await policyRepository.GetPolicyDetailsByUserId(user.Id)).ToList(); - - // Assert - Assert.Equal(2, policyDetails.Count); - - var actualPolicyDetails1 = policyDetails.Find(p => p.OrganizationUserId == orgUser1.Id); - var expectedPolicyDetails1 = new PolicyDetails - { - OrganizationUserId = orgUser1.Id, - OrganizationId = org1.Id, - PolicyType = PolicyType.SingleOrg, - PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = true, IntSetting = 5 }), - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Confirmed, - OrganizationUserPermissionsData = null, - IsProvider = false - }; - Assert.Equivalent(expectedPolicyDetails1, actualPolicyDetails1); - Assert.Equivalent(expectedPolicyDetails1.GetDataModel(), new TestPolicyData { BoolSetting = true, IntSetting = 5 }); - - var actualPolicyDetails2 = policyDetails.Find(p => p.OrganizationUserId == orgUser2.Id); - var expectedPolicyDetails2 = new PolicyDetails - { - OrganizationUserId = orgUser2.Id, - OrganizationId = org2.Id, - PolicyType = PolicyType.SingleOrg, - PolicyData = CoreHelpers.ClassToJsonData(new TestPolicyData { BoolSetting = false, IntSetting = 15 }), - OrganizationUserType = OrganizationUserType.Custom, - OrganizationUserStatus = OrganizationUserStatusType.Accepted, - OrganizationUserPermissionsData = CoreHelpers.ClassToJsonData(new Permissions { ManagePolicies = true }), - IsProvider = false - }; - Assert.Equivalent(expectedPolicyDetails2, actualPolicyDetails2); - Assert.Equivalent(expectedPolicyDetails2.GetDataModel(), new TestPolicyData { BoolSetting = false, IntSetting = 15 }); - Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_InvitedUser_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = null, // invited users have null userId - Status = OrganizationUserStatusType.Invited, - Type = OrganizationUserType.Custom, - Email = user.Email // invited users have matching Email - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Custom, - OrganizationUserStatus = OrganizationUserStatusType.Invited, - IsProvider = false - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - // User has been confirmed to the org but then revoked - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Revoked, - Type = OrganizationUserType.Owner, - Email = null - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Revoked, - IsProvider = false - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - // User has been invited to the org but then revoked - without ever being confirmed and linked to a user. - // This is an unhandled edge case because those users will go through policy enforcement later, - // as part of accepting their invite after being restored. For now this is just documented as expected behavior. - var orgUser = new OrganizationUser - { - OrganizationId = org.Id, - UserId = null, - Status = OrganizationUserStatusType.Revoked, - Type = OrganizationUserType.Owner, - Email = user.Email - }; - await organizationUserRepository.CreateAsync(orgUser); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_SetsIsProvider( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository, - IProviderRepository providerRepository, - IProviderUserRepository providerUserRepository, - IProviderOrganizationRepository providerOrganizationRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Arrange provider - var provider = await providerRepository.CreateAsync(new Provider - { - Name = Guid.NewGuid().ToString(), - Enabled = true - }); - await providerUserRepository.CreateAsync(new ProviderUser - { - ProviderId = provider.Id, - UserId = user.Id, - Status = ProviderUserStatusType.Confirmed - }); - await providerOrganizationRepository.CreateAsync(new ProviderOrganization - { - OrganizationId = org.Id, - ProviderId = provider.Id - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - var expectedPolicyDetails = new PolicyDetails - { - OrganizationUserId = orgUser.Id, - OrganizationId = org.Id, - PolicyType = PolicyType.SingleOrg, - OrganizationUserType = OrganizationUserType.Owner, - OrganizationUserStatus = OrganizationUserStatusType.Confirmed, - IsProvider = true - }; - - Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Org is disabled; its policies remain, but it is now inactive - org.Enabled = false; - await organizationRepository.ReplaceAsync(org); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = true, - Type = PolicyType.SingleOrg, - }); - - // Org is downgraded; its policies remain but its plan no longer supports them - org.UsePolicies = false; - org.PlanType = PlanType.TeamsAnnually; - await organizationRepository.ReplaceAsync(org); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - [Theory, DatabaseData] - public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies( - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository, - IPolicyRepository policyRepository) - { - // Arrange - var user = await userRepository.CreateTestUserAsync(); - var org = await CreateEnterpriseOrg(organizationRepository); - await organizationUserRepository.CreateTestOrganizationUserAsync(org, user); - await policyRepository.CreateAsync(new Policy - { - OrganizationId = org.Id, - Enabled = false, - Type = PolicyType.SingleOrg, - }); - - // Act - var actualPolicyDetails = await policyRepository.GetPolicyDetailsByUserId(user.Id); - - // Assert - Assert.Empty(actualPolicyDetails); - } - - private class TestPolicyData : IPolicyDataModel - { - public bool BoolSetting { get; set; } - public int IntSetting { get; set; } - } - - private Task CreateEnterpriseOrg(IOrganizationRepository organizationRepository) - => organizationRepository.CreateAsync(new Organization - { - Name = Guid.NewGuid().ToString(), - BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl - PlanType = PlanType.EnterpriseAnnually, - UsePolicies = true - }); -} diff --git a/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql b/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql new file mode 100644 index 0000000000..b764824a42 --- /dev/null +++ b/util/Migrator/DbScripts/2025-10-15_00_RefactorPolicyDetailsQueries.sql @@ -0,0 +1,85 @@ +CREATE OR ALTER VIEW [dbo].[UserProviderAccessView] +AS +SELECT DISTINCT + PU.[UserId], + PO.[OrganizationId] +FROM + [dbo].[ProviderUserView] PU +INNER JOIN + [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails] + @UserId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserEmail NVARCHAR(256) + SELECT @UserEmail = Email + FROM + [dbo].[UserView] + WHERE + Id = @UserId + + ;WITH OrgUsers AS + ( + -- All users except invited (Status <> 0): direct UserId match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] <> 0 + AND OU.[UserId] = @UserId + + UNION ALL + + -- Invited users: email match + SELECT + OU.[Id], + OU.[OrganizationId], + OU.[Type], + OU.[Status], + OU.[Permissions] + FROM + [dbo].[OrganizationUserView] OU + WHERE + OU.[Status] = 0 + AND OU.[Email] = @UserEmail + AND @UserEmail IS NOT NULL + ), + Providers AS + ( + SELECT + OrganizationId + FROM + [dbo].[UserProviderAccessView] + WHERE + UserId = @UserId + ) + SELECT + OU.[Id] AS [OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Enabled] AS [PolicyEnabled], + P.[Data] AS [PolicyData], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData], + CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider] + FROM + [dbo].[PolicyView] P + INNER JOIN + OrgUsers OU ON P.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + Providers PR ON PR.[OrganizationId] = OU.[OrganizationId] + WHERE + P.[Type] = @PolicyType +END +GO From b63fdfab0d5106fff77754093789136f18e06dcf Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:42:29 -0500 Subject: [PATCH 41/52] [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 --- .../Controllers/ImportCiphersController.cs | 8 +- .../Helpers/BulkResourceCreationService.cs | 3 + .../ImportCiphersControllerTests.cs | 98 +++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 88028420b7..8b3ec5e26c 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -74,10 +74,14 @@ public class ImportCiphersController : Controller throw new BadRequestException("You cannot import this much data at once."); } + if (model.Ciphers.Any(c => c.ArchivedDate.HasValue)) + { + throw new BadRequestException("You cannot import archived items into an organization."); + } + var orgId = new Guid(organizationId); var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList(); - //An User is allowed to import if CanCreate Collections or has AccessToImportExport var authorized = await CheckOrgImportPermission(collections, orgId); if (!authorized) @@ -156,7 +160,7 @@ public class ImportCiphersController : Controller if (existingCollections.Any() && (await _authorizationService.AuthorizeAsync(User, existingCollections, BulkCollectionOperations.ImportCiphers)).Succeeded) { return true; - }; + } return false; } diff --git a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs index 5a743ba028..2be33e8846 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs @@ -218,6 +218,8 @@ public static class BulkResourceCreationService ciphersTable.Columns.Add(revisionDateColumn); var deletedDateColumn = new DataColumn(nameof(c.DeletedDate), typeof(DateTime)); ciphersTable.Columns.Add(deletedDateColumn); + var archivedDateColumn = new DataColumn(nameof(c.ArchivedDate), typeof(DateTime)); + ciphersTable.Columns.Add(archivedDateColumn); var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short)); ciphersTable.Columns.Add(repromptColumn); var keyColummn = new DataColumn(nameof(c.Key), typeof(string)); @@ -247,6 +249,7 @@ public static class BulkResourceCreationService row[creationDateColumn] = cipher.CreationDate; row[revisionDateColumn] = cipher.RevisionDate; row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value; + row[archivedDateColumn] = cipher.ArchivedDate.HasValue ? cipher.ArchivedDate : DBNull.Value; row[repromptColumn] = cipher.Reprompt.HasValue ? cipher.Reprompt.Value : DBNull.Value; row[keyColummn] = cipher.Key; diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs index 4908bb6847..9ca641a28e 100644 --- a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -75,6 +75,7 @@ public class ImportCiphersControllerTests .With(x => x.Ciphers, fixture.Build() .With(c => c.OrganizationId, Guid.NewGuid().ToString()) .With(c => c.FolderId, Guid.NewGuid().ToString()) + .With(c => c.ArchivedDate, (DateTime?)null) .CreateMany(1).ToArray()) .Create(); @@ -92,6 +93,37 @@ public class ImportCiphersControllerTests ); } + [Theory, BitAutoData] + public async Task PostImportIndividual_WithArchivedDate_SavesArchivedDate(User user, + IFixture fixture, SutProvider sutProvider) + { + var archivedDate = DateTime.UtcNow; + sutProvider.GetDependency() + .SelfHosted = false; + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + var request = fixture.Build() + .With(x => x.Ciphers, fixture.Build() + .With(c => c.ArchivedDate, archivedDate) + .With(c => c.FolderId, (string)null) + .CreateMany(1).ToArray()) + .Create(); + + await sutProvider.Sut.PostImport(request); + + await sutProvider.GetDependency() + .Received() + .ImportIntoIndividualVaultAsync( + Arg.Any>(), + Arg.Is>(ciphers => ciphers.First().ArchivedDate == archivedDate), + Arg.Any>>(), + user.Id + ); + } + /**************************** * PostImport - Organization ****************************/ @@ -156,6 +188,7 @@ public class ImportCiphersControllerTests .With(x => x.Ciphers, fixture.Build() .With(c => c.OrganizationId, Guid.NewGuid().ToString()) .With(c => c.FolderId, Guid.NewGuid().ToString()) + .With(c => c.ArchivedDate, (DateTime?)null) .CreateMany(1).ToArray()) .With(y => y.Collections, fixture.Build() .With(c => c.Id, orgIdGuid) @@ -227,6 +260,7 @@ public class ImportCiphersControllerTests .With(x => x.Ciphers, fixture.Build() .With(c => c.OrganizationId, Guid.NewGuid().ToString()) .With(c => c.FolderId, Guid.NewGuid().ToString()) + .With(c => c.ArchivedDate, (DateTime?)null) .CreateMany(1).ToArray()) .With(y => y.Collections, fixture.Build() .With(c => c.Id, orgIdGuid) @@ -291,6 +325,7 @@ public class ImportCiphersControllerTests .With(x => x.Ciphers, fixture.Build() .With(c => c.OrganizationId, Guid.NewGuid().ToString()) .With(c => c.FolderId, Guid.NewGuid().ToString()) + .With(c => c.ArchivedDate, (DateTime?)null) .CreateMany(1).ToArray()) .With(y => y.Collections, fixture.Build() .With(c => c.Id, orgIdGuid) @@ -354,6 +389,7 @@ public class ImportCiphersControllerTests .With(x => x.Ciphers, fixture.Build() .With(c => c.OrganizationId, Guid.NewGuid().ToString()) .With(c => c.FolderId, Guid.NewGuid().ToString()) + .With(c => c.ArchivedDate, (DateTime?)null) .CreateMany(1).ToArray()) .With(y => y.Collections, fixture.Build() .With(c => c.Id, orgIdGuid) @@ -423,6 +459,7 @@ public class ImportCiphersControllerTests Ciphers = fixture.Build() .With(_ => _.OrganizationId, orgId.ToString()) .With(_ => _.FolderId, Guid.NewGuid().ToString()) + .With(_ => _.ArchivedDate, (DateTime?)null) .CreateMany(2).ToArray(), CollectionRelationships = new List>().ToArray(), }; @@ -499,6 +536,7 @@ public class ImportCiphersControllerTests Ciphers = fixture.Build() .With(_ => _.OrganizationId, orgId.ToString()) .With(_ => _.FolderId, Guid.NewGuid().ToString()) + .With(_ => _.ArchivedDate, (DateTime?)null) .CreateMany(2).ToArray(), CollectionRelationships = new List>().ToArray(), }; @@ -578,6 +616,7 @@ public class ImportCiphersControllerTests Ciphers = fixture.Build() .With(_ => _.OrganizationId, orgId.ToString()) .With(_ => _.FolderId, Guid.NewGuid().ToString()) + .With(_ => _.ArchivedDate, (DateTime?)null) .CreateMany(2).ToArray(), CollectionRelationships = new List>().ToArray(), }; @@ -651,6 +690,7 @@ public class ImportCiphersControllerTests Ciphers = fixture.Build() .With(_ => _.OrganizationId, orgId.ToString()) .With(_ => _.FolderId, Guid.NewGuid().ToString()) + .With(_ => _.ArchivedDate, (DateTime?)null) .CreateMany(2).ToArray(), CollectionRelationships = new List>().ToArray(), }; @@ -720,6 +760,7 @@ public class ImportCiphersControllerTests Ciphers = fixture.Build() .With(_ => _.OrganizationId, orgId.ToString()) .With(_ => _.FolderId, Guid.NewGuid().ToString()) + .With(_ => _.ArchivedDate, (DateTime?)null) .CreateMany(2).ToArray(), CollectionRelationships = new List>().ToArray(), }; @@ -765,6 +806,63 @@ public class ImportCiphersControllerTests Arg.Any()); } + [Theory, BitAutoData] + public async Task PostImportOrganization_ThrowsException_WhenAnyCipherIsArchived( + SutProvider sutProvider, + IFixture fixture, + User user + ) + { + var orgId = Guid.NewGuid(); + + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; + + SetupUserService(sutProvider, user); + + var ciphers = fixture.Build() + .With(_ => _.ArchivedDate, DateTime.UtcNow) + .CreateMany(2).ToArray(); + + var request = new ImportOrganizationCiphersRequestModel + { + Collections = new List().ToArray(), + Ciphers = ciphers, + CollectionRelationships = new List>().ToArray(), + }; + + sutProvider.GetDependency() + .AccessImportExport(Arg.Any()) + .Returns(false); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.ImportCiphers))) + .Returns(AuthorizationResult.Failed()); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + Arg.Any>(), + Arg.Is>(reqs => + reqs.Contains(BulkCollectionOperations.Create))) + .Returns(AuthorizationResult.Success()); + + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(orgId) + .Returns(new List()); + + var exception = await Assert.ThrowsAsync(async () => + { + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); + }); + + Assert.Equal("You cannot import archived items into an organization.", exception.Message); + } + private static void SetupUserService(SutProvider sutProvider, User user) { // This is a workaround for the NSubstitute issue with ambiguous arguments From c58f3d590cbba754758b4feab35bfed5966faa80 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 22 Oct 2025 10:10:44 -0500 Subject: [PATCH 42/52] PM-27204 feature flag for dataDog and crowdStrike (#6473) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index da7b94aada..68f32f8bda 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -245,6 +245,7 @@ public static class FeatureFlagKeys /* DIRT Team */ public const string PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab"; + public const string EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike"; public static List GetAllKeys() { From f82125f4161e8019f0892fd64dcfdeafcd79a651 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Wed, 22 Oct 2025 12:53:46 -0400 Subject: [PATCH 43/52] Clean up workflow files from Zizmor output (#6409) --- .github/workflows/_move_edd_db_scripts.yml | 28 +++++----- .github/workflows/build.yml | 59 +++++++++++---------- .github/workflows/cleanup-after-pr.yml | 12 ++--- .github/workflows/cleanup-rc-branch.yml | 14 ++--- .github/workflows/code-references.yml | 10 ++-- .github/workflows/enforce-labels.yml | 2 +- .github/workflows/load-test.yml | 4 +- .github/workflows/protect-files.yml | 5 +- .github/workflows/publish.yml | 37 +++++++------ .github/workflows/release.yml | 7 ++- .github/workflows/repository-management.yml | 37 +++++++------ .github/workflows/review-code.yml | 12 ++--- .github/workflows/test-database.yml | 14 +++-- .github/workflows/test.yml | 2 + 14 files changed, 140 insertions(+), 103 deletions(-) diff --git a/.github/workflows/_move_edd_db_scripts.yml b/.github/workflows/_move_edd_db_scripts.yml index b38a3e0dff..7e97fa2a07 100644 --- a/.github/workflows/_move_edd_db_scripts.yml +++ b/.github/workflows/_move_edd_db_scripts.yml @@ -41,18 +41,19 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 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: @@ -70,17 +71,18 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 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' }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 907f50197b..49cd81d28f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 @@ -97,23 +98,24 @@ 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 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 @@ -209,8 +211,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 +220,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 +230,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 @@ -260,12 +262,13 @@ 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 @@ -297,6 +300,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 @@ -309,7 +313,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 +336,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 @@ -423,6 +427,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 diff --git a/.github/workflows/cleanup-after-pr.yml b/.github/workflows/cleanup-after-pr.yml index e39bf8ea3a..4e59f1fa96 100644 --- a/.github/workflows/cleanup-after-pr.yml +++ b/.github/workflows/cleanup-after-pr.yml @@ -22,7 +22,7 @@ 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 ########## Remove Docker images ########## - name: Remove the Docker image from ACR @@ -45,20 +45,20 @@ jobs: - Setup - Sso run: | - for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - ) + for SERVICE in $(echo "${SERVICES}" | yq e ".services[]" - ) do - SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}') + 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")' + 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 + az acr repository delete --name "$_AZ_REGISTRY" --image "$SERVICE_NAME:$IMAGE_TAG" --yes else echo "[*] Tag does not exist. No action needed" fi diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index 5c74284423..63079826c7 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -35,6 +35,8 @@ jobs: 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 diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 75e0c43306..35e6cfdd40 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -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: @@ -37,6 +37,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -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 }} diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml index 353127c751..1759b29787 100644 --- a/.github/workflows/enforce-labels.yml +++ b/.github/workflows/enforce-labels.yml @@ -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 diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 9bc6da89e7..cdb53109f5 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -63,13 +63,15 @@ jobs: # 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=${{ steps.get-kv-secrets.outputs.DD-API-KEY }} \ + -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 \ diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 546b8344a6..a939be6fdb 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -34,6 +34,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 444c2289d1..2272387d84 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -105,6 +107,9 @@ jobs: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + persist-credentials: false - name: Set up project name id: setup @@ -112,7 +117,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 +128,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 +145,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 +156,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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bb19b4da1..75b4df4e5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,9 @@ jobs: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + 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 diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 67e1d8a926..92452102cf 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -46,7 +46,7 @@ jobs: BRANCH="hotfix-rc" fi - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" bump_version: name: Bump Version @@ -95,6 +95,7 @@ jobs: with: ref: main token: ${{ steps.app-token.outputs.token }} + persist-credentials: true - name: Configure Git run: | @@ -110,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 != '' }} @@ -120,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 @@ -160,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 @@ -213,13 +218,15 @@ jobs: 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 @@ -227,8 +234,8 @@ 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 diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml index 83cbc3bb54..ec7628d16c 100644 --- a/.github/workflows/review-code.yml +++ b/.github/workflows/review-code.yml @@ -26,14 +26,14 @@ jobs: id: check_changes run: | # Ensure we have the base branch - git fetch origin ${{ github.base_ref }} + git fetch origin "${GITHUB_BASE_REF}" - echo "Comparing changes between origin/${{ github.base_ref }} and HEAD" - CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + echo "Comparing changes between origin/${GITHUB_BASE_REF} and HEAD" + CHANGED_FILES=$(git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD") if [ -z "$CHANGED_FILES" ]; then echo "Zero files changed" - echo "vault_team_changes=false" >> $GITHUB_OUTPUT + echo "vault_team_changes=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -42,7 +42,7 @@ jobs: if [ -z "$VAULT_PATTERNS" ]; then echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS" - echo "vault_team_changes=false" >> $GITHUB_OUTPUT + echo "vault_team_changes=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -72,7 +72,7 @@ jobs: fi done - echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT + echo "vault_team_changes=$vault_team_changes" >> "$GITHUB_OUTPUT" if [ "$vault_team_changes" = "true" ]; then echo "" diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index cdba344195..4a973c0b7c 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -45,6 +45,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 @@ -139,26 +141,26 @@ 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 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 @@ -177,6 +179,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7783fa14b5..36ab8785d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,8 @@ jobs: steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false - name: Set up .NET uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 From 0a7e6ae3ca7a7079ebcf8097651509f8b219c607 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 22 Oct 2025 13:42:56 -0400 Subject: [PATCH 44/52] Billing/pm 27217/update secrets manager test fix (#6478) * fix(billing): Add comments for clarity * fix(billing): Update generated properties for test --- ...UpdateSecretsManagerSubscriptionCommand.cs | 6 ++- ...eSecretsManagerSubscriptionCommandTests.cs | 47 ++++++++++++------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index 739dca5228..d4e1b3cd8d 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -226,7 +226,11 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs // Check minimum seats currently in use by the organization if (organization.SmSeats.Value > update.SmSeats.Value) { + // Retrieve the number of currently occupied Secrets Manager seats for the organization. var occupiedSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); + + // Check if the occupied number of seats exceeds the updated seat count. + // If so, throw an exception indicating that the subscription cannot be decreased below the current usage. if (occupiedSeats > update.SmSeats.Value) { throw new BadRequestException($"{occupiedSeats} users are currently occupying Secrets Manager seats. " + @@ -412,7 +416,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs } /// - /// Requests the number of Secret Manager seats and service accounts are currently used by the organization + /// Requests the number of Secret Manager seats and service accounts currently used by the organization /// /// The id of the organization /// A tuple containing the occupied seats and the occupied service account counts diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index 8b00741215..1e764de6d7 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -278,21 +278,27 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { // Arrange - const int seatCount = 10; - var existingSeatCount = 9; - // Make sure Password Manager seats is greater or equal to Secrets Manager seats - organization.Seats = seatCount; + const int initialSeatCount = 9; + const int maxSeatCount = 20; + // This represents the total number of users allowed in the organization. + organization.Seats = maxSeatCount; + // This represents the number of Secrets Manager users allowed in the organization. + organization.SmSeats = initialSeatCount; + // This represents the upper limit of Secrets Manager seats that can be automatically scaled. + organization.MaxAutoscaleSmSeats = maxSeatCount; + + organization.PlanType = PlanType.EnterpriseAnnually; var plan = StaticStore.GetPlan(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { - SmSeats = seatCount, - MaxAutoscaleSmSeats = seatCount + SmSeats = 8, + MaxAutoscaleSmSeats = maxSeatCount }; sutProvider.GetDependency() .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) - .Returns(existingSeatCount); + .Returns(5); // Act await sutProvider.Sut.UpdateSubscriptionAsync(update); @@ -316,21 +322,29 @@ public class UpdateSecretsManagerSubscriptionCommandTests SutProvider sutProvider) { // Arrange - const int seatCount = 10; - const int existingSeatCount = 10; - var ownerDetailsList = new List { new() { Email = "owner@example.com" } }; + const int initialSeatCount = 5; + const int maxSeatCount = 10; - // The amount of seats for users in an organization + // This represents the total number of users allowed in the organization. + organization.Seats = maxSeatCount; + // This represents the number of Secrets Manager users allowed in the organization. + organization.SmSeats = initialSeatCount; + // This represents the upper limit of Secrets Manager seats that can be automatically scaled. + organization.MaxAutoscaleSmSeats = maxSeatCount; + + var ownerDetailsList = new List { new() { Email = "owner@example.com" } }; + organization.PlanType = PlanType.EnterpriseAnnually; var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { - SmSeats = seatCount, - MaxAutoscaleSmSeats = seatCount + SmSeats = maxSeatCount, + MaxAutoscaleSmSeats = maxSeatCount }; sutProvider.GetDependency() .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) - .Returns(existingSeatCount); + .Returns(maxSeatCount); sutProvider.GetDependency() .GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner) .Returns(ownerDetailsList); @@ -340,15 +354,14 @@ public class UpdateSecretsManagerSubscriptionCommandTests // Assert - // Currently being called once each for different validation methods await sutProvider.GetDependency() - .Received(2) + .Received(1) .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); await sutProvider.GetDependency() .Received(1) .SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Is(organization), - Arg.Is(seatCount), + Arg.Is(maxSeatCount), Arg.Is>(emails => emails.Contains(ownerDetailsList[0].Email))); } From 6a3fc0895795c4b636369466c24a5168ad69fcd4 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:13:16 -0500 Subject: [PATCH 45/52] [PM-26793] Fetch premium plan from pricing service (#6450) * Fetch premium plan from pricing service * Run dotnet format --- .../Controllers/PlansController.cs | 9 +- ...tePremiumCloudHostedSubscriptionCommand.cs | 10 +- .../Commands/PreviewPremiumTaxCommand.cs | 11 ++- src/Core/Billing/Pricing/IPricingClient.cs | 17 +++- .../{Models => Organizations}/Feature.cs | 2 +- .../Pricing/{Models => Organizations}/Plan.cs | 2 +- .../{ => Organizations}/PlanAdapter.cs | 4 +- .../{Models => Organizations}/Purchasable.cs | 2 +- src/Core/Billing/Pricing/Premium/Plan.cs | 10 ++ .../Billing/Pricing/Premium/Purchasable.cs | 7 ++ src/Core/Billing/Pricing/PricingClient.cs | 92 +++++++++++++------ .../PremiumUserBillingService.cs | 10 +- src/Core/Constants.cs | 1 + .../Implementations/StripePaymentService.cs | 29 ++++-- .../Services/Implementations/UserService.cs | 12 ++- ...miumCloudHostedSubscriptionCommandTests.cs | 19 +++- .../Commands/PreviewPremiumTaxCommandTests.cs | 17 +++- 17 files changed, 191 insertions(+), 63 deletions(-) rename src/Api/{ => Billing}/Controllers/PlansController.cs (69%) rename src/Core/Billing/Pricing/{Models => Organizations}/Feature.cs (69%) rename src/Core/Billing/Pricing/{Models => Organizations}/Plan.cs (94%) rename src/Core/Billing/Pricing/{ => Organizations}/PlanAdapter.cs (98%) rename src/Core/Billing/Pricing/{Models => Organizations}/Purchasable.cs (99%) create mode 100644 src/Core/Billing/Pricing/Premium/Plan.cs create mode 100644 src/Core/Billing/Pricing/Premium/Purchasable.cs diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Billing/Controllers/PlansController.cs similarity index 69% rename from src/Api/Controllers/PlansController.cs rename to src/Api/Billing/Controllers/PlansController.cs index 11b070fb66..d43a1e6044 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Billing/Controllers/PlansController.cs @@ -3,7 +3,7 @@ using Bit.Core.Billing.Pricing; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Controllers; +namespace Bit.Api.Billing.Controllers; [Route("plans")] [Authorize("Web")] @@ -18,4 +18,11 @@ public class PlansController( var responses = plans.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } + + [HttpGet("premium")] + public async Task GetPremiumPlanAsync() + { + var premiumPlan = await pricingClient.GetAvailablePremiumPlan(); + return TypedResults.Ok(premiumPlan); + } } diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs index c5fdc3287a..fa01acabda 100644 --- a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -50,7 +51,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand( ISubscriberService subscriberService, IUserService userService, IPushNotificationService pushNotificationService, - ILogger logger) + ILogger logger, + IPricingClient pricingClient) : BaseBillingCommand(logger), ICreatePremiumCloudHostedSubscriptionCommand { private static readonly List _expand = ["tax"]; @@ -255,11 +257,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand( Customer customer, int? storage) { + var premiumPlan = await pricingClient.GetAvailablePremiumPlan(); + var subscriptionItemOptionsList = new List { new () { - Price = StripeConstants.Prices.PremiumAnnually, + Price = premiumPlan.Seat.StripePriceId, Quantity = 1 } }; @@ -268,7 +272,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand( { subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = StripeConstants.Prices.StoragePlanPersonal, + Price = premiumPlan.Storage.StripePriceId, Quantity = storage }); } diff --git a/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs index 9275bcf3d9..5f09b8b77b 100644 --- a/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs +++ b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs @@ -1,14 +1,12 @@ using Bit.Core.Billing.Commands; -using Bit.Core.Billing.Constants; using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Pricing; using Bit.Core.Services; using Microsoft.Extensions.Logging; using Stripe; namespace Bit.Core.Billing.Premium.Commands; -using static StripeConstants; - public interface IPreviewPremiumTaxCommand { Task> Run( @@ -18,6 +16,7 @@ public interface IPreviewPremiumTaxCommand public class PreviewPremiumTaxCommand( ILogger logger, + IPricingClient pricingClient, IStripeAdapter stripeAdapter) : BaseBillingCommand(logger), IPreviewPremiumTaxCommand { public Task> Run( @@ -25,6 +24,8 @@ public class PreviewPremiumTaxCommand( BillingAddress billingAddress) => HandleAsync<(decimal, decimal)>(async () => { + var premiumPlan = await pricingClient.GetAvailablePremiumPlan(); + var options = new InvoiceCreatePreviewOptions { AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }, @@ -41,7 +42,7 @@ public class PreviewPremiumTaxCommand( { Items = [ - new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 } + new InvoiceSubscriptionDetailsItemOptions { Price = premiumPlan.Seat.StripePriceId, Quantity = 1 } ] } }; @@ -50,7 +51,7 @@ public class PreviewPremiumTaxCommand( { options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions { - Price = Prices.StoragePlanPersonal, + Price = premiumPlan.Storage.StripePriceId, Quantity = additionalStorage }); } diff --git a/src/Core/Billing/Pricing/IPricingClient.cs b/src/Core/Billing/Pricing/IPricingClient.cs index bc3f142dda..18588ae432 100644 --- a/src/Core/Billing/Pricing/IPricingClient.cs +++ b/src/Core/Billing/Pricing/IPricingClient.cs @@ -3,12 +3,14 @@ using Bit.Core.Exceptions; using Bit.Core.Models.StaticStore; using Bit.Core.Utilities; -#nullable enable - namespace Bit.Core.Billing.Pricing; +using OrganizationPlan = Plan; +using PremiumPlan = Premium.Plan; + public interface IPricingClient { + // TODO: Rename with Organization focus. /// /// Retrieve a Bitwarden plan by its . If the feature flag 'use-pricing-service' is enabled, /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . @@ -16,8 +18,9 @@ public interface IPricingClient /// The type of plan to retrieve. /// A Bitwarden record or null in the case the plan could not be found or the method was executed from a self-hosted instance. /// Thrown when the request to the Pricing Service fails unexpectedly. - Task GetPlan(PlanType planType); + Task GetPlan(PlanType planType); + // TODO: Rename with Organization focus. /// /// Retrieve a Bitwarden plan by its . If the feature flag 'use-pricing-service' is enabled, /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . @@ -26,13 +29,17 @@ public interface IPricingClient /// A Bitwarden record. /// Thrown when the for the provided could not be found or the method was executed from a self-hosted instance. /// Thrown when the request to the Pricing Service fails unexpectedly. - Task GetPlanOrThrow(PlanType planType); + Task GetPlanOrThrow(PlanType planType); + // TODO: Rename with Organization focus. /// /// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled, /// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing . /// /// A list of Bitwarden records or an empty list in the case the method is executed from a self-hosted instance. /// Thrown when the request to the Pricing Service fails unexpectedly. - Task> ListPlans(); + Task> ListPlans(); + + Task GetAvailablePremiumPlan(); + Task> ListPremiumPlans(); } diff --git a/src/Core/Billing/Pricing/Models/Feature.cs b/src/Core/Billing/Pricing/Organizations/Feature.cs similarity index 69% rename from src/Core/Billing/Pricing/Models/Feature.cs rename to src/Core/Billing/Pricing/Organizations/Feature.cs index ea9da5217d..df10d2bcf8 100644 --- a/src/Core/Billing/Pricing/Models/Feature.cs +++ b/src/Core/Billing/Pricing/Organizations/Feature.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Pricing.Models; +namespace Bit.Core.Billing.Pricing.Organizations; public class Feature { diff --git a/src/Core/Billing/Pricing/Models/Plan.cs b/src/Core/Billing/Pricing/Organizations/Plan.cs similarity index 94% rename from src/Core/Billing/Pricing/Models/Plan.cs rename to src/Core/Billing/Pricing/Organizations/Plan.cs index 5b4296474b..c533c271cb 100644 --- a/src/Core/Billing/Pricing/Models/Plan.cs +++ b/src/Core/Billing/Pricing/Organizations/Plan.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Pricing.Models; +namespace Bit.Core.Billing.Pricing.Organizations; public class Plan { diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/Organizations/PlanAdapter.cs similarity index 98% rename from src/Core/Billing/Pricing/PlanAdapter.cs rename to src/Core/Billing/Pricing/Organizations/PlanAdapter.cs index 560987b891..390f7b2146 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/Organizations/PlanAdapter.cs @@ -1,8 +1,6 @@ using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing.Models; -using Plan = Bit.Core.Billing.Pricing.Models.Plan; -namespace Bit.Core.Billing.Pricing; +namespace Bit.Core.Billing.Pricing.Organizations; public record PlanAdapter : Core.Models.StaticStore.Plan { diff --git a/src/Core/Billing/Pricing/Models/Purchasable.cs b/src/Core/Billing/Pricing/Organizations/Purchasable.cs similarity index 99% rename from src/Core/Billing/Pricing/Models/Purchasable.cs rename to src/Core/Billing/Pricing/Organizations/Purchasable.cs index 7cb4ee00c1..f6704394f7 100644 --- a/src/Core/Billing/Pricing/Models/Purchasable.cs +++ b/src/Core/Billing/Pricing/Organizations/Purchasable.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; using OneOf; -namespace Bit.Core.Billing.Pricing.Models; +namespace Bit.Core.Billing.Pricing.Organizations; [JsonConverter(typeof(PurchasableJsonConverter))] public class Purchasable(OneOf input) : OneOfBase(input) diff --git a/src/Core/Billing/Pricing/Premium/Plan.cs b/src/Core/Billing/Pricing/Premium/Plan.cs new file mode 100644 index 0000000000..f377157363 --- /dev/null +++ b/src/Core/Billing/Pricing/Premium/Plan.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Billing.Pricing.Premium; + +public class Plan +{ + public string Name { get; init; } = null!; + public int? LegacyYear { get; init; } + public bool Available { get; init; } + public Purchasable Seat { get; init; } = null!; + public Purchasable Storage { get; init; } = null!; +} diff --git a/src/Core/Billing/Pricing/Premium/Purchasable.cs b/src/Core/Billing/Pricing/Premium/Purchasable.cs new file mode 100644 index 0000000000..633eb2e8aa --- /dev/null +++ b/src/Core/Billing/Pricing/Premium/Purchasable.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Pricing.Premium; + +public class Purchasable +{ + public string StripePriceId { get; init; } = null!; + public decimal Price { get; init; } +} diff --git a/src/Core/Billing/Pricing/PricingClient.cs b/src/Core/Billing/Pricing/PricingClient.cs index a3db8ce07f..d2630ea43b 100644 --- a/src/Core/Billing/Pricing/PricingClient.cs +++ b/src/Core/Billing/Pricing/PricingClient.cs @@ -1,24 +1,27 @@ using System.Net; using System.Net.Http.Json; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing.Organizations; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; -using Plan = Bit.Core.Models.StaticStore.Plan; - -#nullable enable namespace Bit.Core.Billing.Pricing; +using OrganizationPlan = Bit.Core.Models.StaticStore.Plan; +using PremiumPlan = Premium.Plan; +using Purchasable = Premium.Purchasable; + public class PricingClient( IFeatureService featureService, GlobalSettings globalSettings, HttpClient httpClient, ILogger logger) : IPricingClient { - public async Task GetPlan(PlanType planType) + public async Task GetPlan(PlanType planType) { if (globalSettings.SelfHosted) { @@ -40,16 +43,14 @@ public class PricingClient( return null; } - var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}"); + var response = await httpClient.GetAsync($"plans/organization/{lookupKey}"); if (response.IsSuccessStatusCode) { - var plan = await response.Content.ReadFromJsonAsync(); - if (plan == null) - { - throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); - } - return new PlanAdapter(plan); + var plan = await response.Content.ReadFromJsonAsync(); + return plan == null + ? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null") + : new PlanAdapter(plan); } if (response.StatusCode == HttpStatusCode.NotFound) @@ -62,19 +63,14 @@ public class PricingClient( message: $"Request to the Pricing Service failed with status code {response.StatusCode}"); } - public async Task GetPlanOrThrow(PlanType planType) + public async Task GetPlanOrThrow(PlanType planType) { var plan = await GetPlan(planType); - if (plan == null) - { - throw new NotFoundException(); - } - - return plan; + return plan ?? throw new NotFoundException($"Could not find plan for type {planType}"); } - public async Task> ListPlans() + public async Task> ListPlans() { if (globalSettings.SelfHosted) { @@ -88,16 +84,51 @@ public class PricingClient( return StaticStore.Plans.ToList(); } - var response = await httpClient.GetAsync("plans"); + var response = await httpClient.GetAsync("plans/organization"); if (response.IsSuccessStatusCode) { - var plans = await response.Content.ReadFromJsonAsync>(); - if (plans == null) - { - throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); - } - return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList(); + var plans = await response.Content.ReadFromJsonAsync>(); + return plans == null + ? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null") + : plans.Select(OrganizationPlan (plan) => new PlanAdapter(plan)).ToList(); + } + + throw new BillingException( + message: $"Request to the Pricing Service failed with status {response.StatusCode}"); + } + + public async Task GetAvailablePremiumPlan() + { + var premiumPlans = await ListPremiumPlans(); + + var availablePlan = premiumPlans.FirstOrDefault(premiumPlan => premiumPlan.Available); + + return availablePlan ?? throw new NotFoundException("Could not find available premium plan"); + } + + public async Task> ListPremiumPlans() + { + if (globalSettings.SelfHosted) + { + return []; + } + + var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService); + var fetchPremiumPriceFromPricingService = + featureService.IsEnabled(FeatureFlagKeys.PM26793_FetchPremiumPriceFromPricingService); + + if (!usePricingService || !fetchPremiumPriceFromPricingService) + { + return [CurrentPremiumPlan]; + } + + var response = await httpClient.GetAsync("plans/premium"); + + if (response.IsSuccessStatusCode) + { + var plans = await response.Content.ReadFromJsonAsync>(); + return plans ?? throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); } throw new BillingException( @@ -130,4 +161,13 @@ public class PricingClient( PlanType.TeamsStarter2023 => "teams-starter-2023", _ => null }; + + private static PremiumPlan CurrentPremiumPlan => new() + { + Name = "Premium", + Available = true, + LegacyYear = null, + Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, + Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal } + }; } diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index e7e67c0a11..3170060de4 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -6,6 +6,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; @@ -30,7 +31,8 @@ public class PremiumUserBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository) : IPremiumUserBillingService + IUserRepository userRepository, + IPricingClient pricingClient) : IPremiumUserBillingService { public async Task Credit(User user, decimal amount) { @@ -301,11 +303,13 @@ public class PremiumUserBillingService( Customer customer, int? storage) { + var premiumPlan = await pricingClient.GetAvailablePremiumPlan(); + var subscriptionItemOptionsList = new List { new () { - Price = StripeConstants.Prices.PremiumAnnually, + Price = premiumPlan.Seat.StripePriceId, Quantity = 1 } }; @@ -314,7 +318,7 @@ public class PremiumUserBillingService( { subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = StripeConstants.Prices.StoragePlanPersonal, + Price = premiumPlan.Storage.StripePriceId, Quantity = storage }); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 68f32f8bda..5a600e26bf 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -185,6 +185,7 @@ public static class FeatureFlagKeys public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button"; public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog"; public const string PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page"; + public const string PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service"; /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index bb53933d02..2707401134 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Responses; @@ -896,11 +897,14 @@ public class StripePaymentService : IPaymentService } } + [Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")] public async Task PreviewInvoiceAsync( PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId) { + var premiumPlan = await _pricingClient.GetAvailablePremiumPlan(); + var options = new InvoiceCreatePreviewOptions { AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, }, @@ -909,8 +913,17 @@ public class StripePaymentService : IPaymentService { Items = [ - new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = StripeConstants.Prices.PremiumAnnually }, - new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = StripeConstants.Prices.StoragePlanPersonal } + new InvoiceSubscriptionDetailsItemOptions + { + Quantity = 1, + Plan = premiumPlan.Seat.StripePriceId + }, + + new InvoiceSubscriptionDetailsItemOptions + { + Quantity = parameters.PasswordManager.AdditionalStorage, + Plan = premiumPlan.Storage.StripePriceId + } ] }, CustomerDetails = new InvoiceCustomerDetailsOptions @@ -1028,7 +1041,7 @@ public class StripePaymentService : IPaymentService { Items = [ - new() + new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.AdditionalStorage, Plan = plan.PasswordManager.StripeStoragePlanId @@ -1049,7 +1062,7 @@ public class StripePaymentService : IPaymentService { var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(parameters.PasswordManager.SponsoredPlan.Value); options.SubscriptionDetails.Items.Add( - new() { Quantity = 1, Plan = sponsoredPlan.StripePlanId } + new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId } ); } else @@ -1057,13 +1070,13 @@ public class StripePaymentService : IPaymentService if (plan.PasswordManager.HasAdditionalSeatsOption) { options.SubscriptionDetails.Items.Add( - new() { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId } + new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId } ); } else { options.SubscriptionDetails.Items.Add( - new() { Quantity = 1, Plan = plan.PasswordManager.StripePlanId } + new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId } ); } @@ -1071,7 +1084,7 @@ public class StripePaymentService : IPaymentService { if (plan.SecretsManager.HasAdditionalSeatsOption) { - options.SubscriptionDetails.Items.Add(new() + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.SecretsManager?.Seats ?? 0, Plan = plan.SecretsManager.StripeSeatPlanId @@ -1080,7 +1093,7 @@ public class StripePaymentService : IPaymentService if (plan.SecretsManager.HasAdditionalServiceAccountOption) { - options.SubscriptionDetails.Items.Add(new() + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0, Plan = plan.SecretsManager.StripeServiceAccountPlanId diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index a36b9e37cc..daf1b2078d 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -14,10 +14,10 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; @@ -72,6 +72,7 @@ public class UserService : UserManager, IUserService private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDistributedCache _distributedCache; private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IPricingClient _pricingClient; public UserService( IUserRepository userRepository, @@ -106,7 +107,8 @@ public class UserService : UserManager, IUserService IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IDistributedCache distributedCache, - IPolicyRequirementQuery policyRequirementQuery) + IPolicyRequirementQuery policyRequirementQuery, + IPricingClient pricingClient) : base( store, optionsAccessor, @@ -146,6 +148,7 @@ public class UserService : UserManager, IUserService _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _distributedCache = distributedCache; _policyRequirementQuery = policyRequirementQuery; + _pricingClient = pricingClient; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -972,8 +975,9 @@ public class UserService : UserManager, IUserService throw new BadRequestException("Not a premium user."); } - var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, - StripeConstants.Prices.StoragePlanPersonal); + var premiumPlan = await _pricingClient.GetAvailablePremiumPlan(); + + var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId); await SaveUserAsync(user); return secret; } diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs index 8504d3122a..b6d497b7de 100644 --- a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs @@ -1,7 +1,9 @@ using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Platform.Push; @@ -14,6 +16,8 @@ using NSubstitute; using Stripe; using Xunit; using Address = Stripe.Address; +using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan; +using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable; using StripeCustomer = Stripe.Customer; using StripeSubscription = Stripe.Subscription; @@ -28,6 +32,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests private readonly ISubscriberService _subscriberService = Substitute.For(); private readonly IUserService _userService = Substitute.For(); private readonly IPushNotificationService _pushNotificationService = Substitute.For(); + private readonly IPricingClient _pricingClient = Substitute.For(); private readonly CreatePremiumCloudHostedSubscriptionCommand _command; public CreatePremiumCloudHostedSubscriptionCommandTests() @@ -36,6 +41,17 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests baseServiceUri.CloudRegion.Returns("US"); _globalSettings.BaseServiceUri.Returns(baseServiceUri); + // Setup default premium plan with standard pricing + var premiumPlan = new PremiumPlan + { + Name = "Premium", + Available = true, + LegacyYear = null, + Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, + Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal } + }; + _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan); + _command = new CreatePremiumCloudHostedSubscriptionCommand( _braintreeGateway, _globalSettings, @@ -44,7 +60,8 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests _subscriberService, _userService, _pushNotificationService, - Substitute.For>()); + Substitute.For>(), + _pricingClient); } [Theory, BitAutoData] diff --git a/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs index 9e919a83f9..d0b2eb7aa4 100644 --- a/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs @@ -1,23 +1,38 @@ using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Billing.Pricing; using Bit.Core.Services; using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; using static Bit.Core.Billing.Constants.StripeConstants; +using PremiumPlan = Bit.Core.Billing.Pricing.Premium.Plan; +using PremiumPurchasable = Bit.Core.Billing.Pricing.Premium.Purchasable; namespace Bit.Core.Test.Billing.Premium.Commands; public class PreviewPremiumTaxCommandTests { private readonly ILogger _logger = Substitute.For>(); + private readonly IPricingClient _pricingClient = Substitute.For(); private readonly IStripeAdapter _stripeAdapter = Substitute.For(); private readonly PreviewPremiumTaxCommand _command; public PreviewPremiumTaxCommandTests() { - _command = new PreviewPremiumTaxCommand(_logger, _stripeAdapter); + // Setup default premium plan with standard pricing + var premiumPlan = new PremiumPlan + { + Name = "Premium", + Available = true, + LegacyYear = null, + Seat = new PremiumPurchasable { Price = 10M, StripePriceId = Prices.PremiumAnnually }, + Storage = new PremiumPurchasable { Price = 4M, StripePriceId = Prices.StoragePlanPersonal } + }; + _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan); + + _command = new PreviewPremiumTaxCommand(_logger, _pricingClient, _stripeAdapter); } [Fact] From 9ce1ecba49d6e1f63f867ccbba7d303547892743 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:13:31 -0400 Subject: [PATCH 46/52] [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> --- src/Core/Constants.cs | 1 + .../Auth/SendAccessEmailOtpEmailv2.html.hbs | 675 ++++++++++++++++++ .../Auth/SendAccessEmailOtpEmailv2.text.hbs | 9 + src/Core/MailTemplates/Mjml/.mjmlconfig | 2 +- .../MailTemplates/Mjml/components/footer.mjml | 20 +- .../MailTemplates/Mjml/components/head.mjml | 8 +- .../MailTemplates/Mjml/components/hero.js | 64 -- .../Mjml/components/learn-more-footer.mjml | 18 + .../Mjml/components/mj-bw-hero.js | 100 +++ .../Mjml/emails/Auth/send-email-otp.mjml | 64 ++ .../Mjml/emails/{ => Auth}/two-factor.mjml | 6 +- .../MailTemplates/Mjml/emails/invite.mjml | 21 +- .../Mail/Auth/DefaultEmailOtpViewModel.cs | 1 + src/Core/Services/IMailService.cs | 11 + .../Implementations/HandlebarsMailService.cs | 21 + .../NoopImplementations/NoopMailService.cs | 5 + .../SendEmailOtpRequestValidator.cs | 21 +- .../SendEmailOtpRequestValidatorTests.cs | 3 +- 18 files changed, 945 insertions(+), 105 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs delete mode 100644 src/Core/MailTemplates/Mjml/components/hero.js create mode 100644 src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml create mode 100644 src/Core/MailTemplates/Mjml/components/mj-bw-hero.js create mode 100644 src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml rename src/Core/MailTemplates/Mjml/emails/{ => Auth}/two-factor.mjml (77%) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5a600e26bf..f7ce3aa59e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -154,6 +154,7 @@ public static class FeatureFlagKeys public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods"; public const string PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword = "pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password"; + public const string MJMLBasedEmailTemplates = "mjml-based-email-templates"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs new file mode 100644 index 0000000000..095cdc82d7 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.html.hbs @@ -0,0 +1,675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +

+ Verify your email to access this Bitwarden Send +

+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
Your verification code is:
+ +
+ +
{{Token}}
+ +
+ +
+ +
+ +
This code expires in {{Expiry}} minutes. After that, you'll need to + verify your email again.
+ +
+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + + + + +
+ + +
+ + + + + + + +
+ + + + + + + + + +
+ +

+ Bitwarden Send transmits sensitive, temporary information to + others easily and securely. Learn more about + Bitwarden Send + or + sign up + to try it today. +

+ +
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ + + + + + + +
+ + +
+ + + + + + + + + +
+ +

+ Learn more about Bitwarden +

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center.
+ +
+ +
+ + + +
+ + + + + + + + + +
+ + + + + + + +
+ + + +
+ +
+ +
+ + +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ + + + + + + +
+ + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+
+ + + +
+ +

+ © 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa + Barbara, CA, USA +

+

+ Always confirm you are on a trusted Bitwarden domain before logging + in:
+ bitwarden.com | + Learn why we include this +

+ +
+ +
+ + +
+ +
+ + + + + +
+ + + + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs new file mode 100644 index 0000000000..7c9c1db527 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmailv2.text.hbs @@ -0,0 +1,9 @@ +{{#>BasicTextLayout}} +Verify your email to access this Bitwarden Send. + +Your verification code is: {{Token}} + +This code can only be used once and expires in {{Expiry}} minutes. After that you'll need to verify your email again. + +Bitwarden Send transmits sensitive, temporary information to others easily and securely. Learn more about Bitwarden Send or sign up to try it today. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Mjml/.mjmlconfig b/src/Core/MailTemplates/Mjml/.mjmlconfig index 7560e0fb96..c382f10a12 100644 --- a/src/Core/MailTemplates/Mjml/.mjmlconfig +++ b/src/Core/MailTemplates/Mjml/.mjmlconfig @@ -1,5 +1,5 @@ { "packages": [ - "components/hero" + "components/mj-bw-hero" ] } diff --git a/src/Core/MailTemplates/Mjml/components/footer.mjml b/src/Core/MailTemplates/Mjml/components/footer.mjml index 0634033618..2b2268f33b 100644 --- a/src/Core/MailTemplates/Mjml/components/footer.mjml +++ b/src/Core/MailTemplates/Mjml/components/footer.mjml @@ -2,38 +2,38 @@ @@ -45,8 +45,8 @@

Always confirm you are on a trusted Bitwarden domain before logging in:
- bitwarden.com | - Learn why we include this + bitwarden.com | + Learn why we include this

diff --git a/src/Core/MailTemplates/Mjml/components/head.mjml b/src/Core/MailTemplates/Mjml/components/head.mjml index 389ae77c12..cf78cd6223 100644 --- a/src/Core/MailTemplates/Mjml/components/head.mjml +++ b/src/Core/MailTemplates/Mjml/components/head.mjml @@ -4,7 +4,7 @@ font-size="16px" /> - + @@ -22,3 +22,9 @@ border-radius: 3px; } + + + +@media only screen and + (max-width: 480px) { .hide-small-img { display: none !important; } .send-bubble { padding-left: 20px; padding-right: 20px; width: 90% !important; } } + diff --git a/src/Core/MailTemplates/Mjml/components/hero.js b/src/Core/MailTemplates/Mjml/components/hero.js deleted file mode 100644 index 6c5bd9bc99..0000000000 --- a/src/Core/MailTemplates/Mjml/components/hero.js +++ /dev/null @@ -1,64 +0,0 @@ -const { BodyComponent } = require("mjml-core"); -class MjBwHero extends BodyComponent { - static dependencies = { - // Tell the validator which tags are allowed as our component's parent - "mj-column": ["mj-bw-hero"], - "mj-wrapper": ["mj-bw-hero"], - // Tell the validator which tags are allowed as our component's children - "mj-bw-hero": [], - }; - - static allowedAttributes = { - "img-src": "string", - title: "string", - "button-text": "string", - "button-url": "string", - }; - - static defaultAttributes = {}; - - render() { - return this.renderMJML(` - - - - -

- ${this.getAttribute("title")} -

-
- - ${this.getAttribute("button-text")} - -
- - - -
- `); - } -} - -module.exports = MjBwHero; diff --git a/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml b/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml new file mode 100644 index 0000000000..9df0614aae --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/learn-more-footer.mjml @@ -0,0 +1,18 @@ + + + +

+ Learn more about Bitwarden +

+ Find user guides, product documentation, and videos on the + Bitwarden Help Center. +
+
+ + + +
diff --git a/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js b/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js new file mode 100644 index 0000000000..d329d4ea38 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/mj-bw-hero.js @@ -0,0 +1,100 @@ +const { BodyComponent } = require("mjml-core"); +class MjBwHero extends BodyComponent { + static dependencies = { + // Tell the validator which tags are allowed as our component's parent + "mj-column": ["mj-bw-hero"], + "mj-wrapper": ["mj-bw-hero"], + // Tell the validator which tags are allowed as our component's children + "mj-bw-hero": [], + }; + + static allowedAttributes = { + "img-src": "string", // REQUIRED: Source for the image displayed in the right-hand side of the blue header area + title: "string", // REQUIRED: large text stating primary purpose of the email + "button-text": "string", // OPTIONAL: text to display in the button + "button-url": "string", // OPTIONAL: URL to navigate to when the button is clicked + "sub-title": "string", // OPTIONAL: smaller text providing additional context for the title + }; + + static defaultAttributes = {}; + + render() { + if (this.getAttribute("button-text") && this.getAttribute("button-url")) { + return this.renderMJML(` + + + + +

+ ${this.getAttribute("title")} +

+
+ + ${this.getAttribute("button-text")} + +
+ + + +
+ `); + } else { + return this.renderMJML(` + + + + +

+ ${this.getAttribute("title")} +

+
+
+ + + +
+ `); + } + } +} + +module.exports = MjBwHero; diff --git a/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml b/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml new file mode 100644 index 0000000000..6ccc481ff8 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/Auth/send-email-otp.mjml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + Your verification code is: + {{Token}} + + + This code expires in {{Expiry}} minutes. After that, you'll need to + verify your email again. + + + + + + +

+ Bitwarden Send transmits sensitive, temporary information to + others easily and securely. Learn more about + Bitwarden Send + or + sign up + to try it today. +

+
+
+
+
+ + + + + + + + +
+
diff --git a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml b/src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml similarity index 77% rename from src/Core/MailTemplates/Mjml/emails/two-factor.mjml rename to src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml index 5091e208d3..3b63c278fc 100644 --- a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml +++ b/src/Core/MailTemplates/Mjml/emails/Auth/two-factor.mjml @@ -1,10 +1,10 @@ - + - + - + diff --git a/src/Core/MailTemplates/Mjml/emails/invite.mjml b/src/Core/MailTemplates/Mjml/emails/invite.mjml index 4eae12d0dc..cdace39c95 100644 --- a/src/Core/MailTemplates/Mjml/emails/invite.mjml +++ b/src/Core/MailTemplates/Mjml/emails/invite.mjml @@ -22,26 +22,7 @@
- - - -

- We’re here for you! -

- If you have any questions, search the Bitwarden - Help - site or - contact us. -
-
- - - -
+ diff --git a/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs index 5faf550e60..5eabd5ba2c 100644 --- a/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs +++ b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs @@ -9,4 +9,5 @@ public class DefaultEmailOtpViewModel : BaseMailModel public string? TheDate { get; set; } public string? TheTime { get; set; } public string? TimeZone { get; set; } + public string? Expiry { get; set; } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 6e61c4f8dd..5a3428c25a 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -31,6 +32,16 @@ public interface IMailService Task SendChangeEmailEmailAsync(string newEmailAddress, string token); Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose); Task SendSendEmailOtpEmailAsync(string email, string token, string subject); + /// + /// has a default expiry of 5 minutes so we set the expiry to that value int he view model. + /// Sends OTP code token to the specified email address. + /// will replace when MJML templates are fully accepted. + /// + /// Email address to send the OTP to + /// Otp code token + /// subject line of the email + /// Task + Task SendSendEmailOtpEmailv2Async(string email, string token, string subject); Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType type, DateTime utcNow, string ip); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 75e0c78702..19705766ed 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -224,6 +224,27 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendSendEmailOtpEmailv2Async(string email, string token, string subject) + { + var message = CreateDefaultMessage(subject, email); + var requestDateTime = DateTime.UtcNow; + var model = new DefaultEmailOtpViewModel + { + Token = token, + Expiry = "5", // This should be configured through the OTPDefaultTokenProviderOptions but for now we will hardcode it to 5 minutes. + TheDate = requestDateTime.ToLongDateString(), + TheTime = requestDateTime.ToShortTimeString(), + TimeZone = _utcTimeZoneDisplay, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + }; + await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmailv2", model); + message.MetaData.Add("SendGridBypassListManagement", true); + // TODO - PM-25380 change to string constant + message.Category = "SendEmailOtp"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip) { // Check if we've sent this email within the last hour diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 7ec05bb1f9..1459fab966 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -98,6 +98,11 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendSendEmailOtpEmailv2Async(string email, string token, string subject) + { + return Task.FromResult(0); + } + public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip) { return Task.FromResult(0); diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs index ca48c4fbec..34a7a6f6e7 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Bit.Core; using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Services; @@ -10,6 +11,7 @@ using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; public class SendEmailOtpRequestValidator( + IFeatureService featureService, IOtpTokenProvider otpTokenProvider, IMailService mailService) : ISendAuthenticationMethodValidator { @@ -60,11 +62,20 @@ public class SendEmailOtpRequestValidator( { return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed); } - - await mailService.SendSendEmailOtpEmailAsync( - email, - token, - string.Format(SendAccessConstants.OtpEmail.Subject, token)); + if (featureService.IsEnabled(FeatureFlagKeys.MJMLBasedEmailTemplates)) + { + await mailService.SendSendEmailOtpEmailv2Async( + email, + token, + string.Format(SendAccessConstants.OtpEmail.Subject, token)); + } + else + { + await mailService.SendSendEmailOtpEmailAsync( + email, + token, + string.Format(SendAccessConstants.OtpEmail.Subject, token)); + } return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent); } diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs index 46f61cb333..7fdfacf428 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs @@ -265,9 +265,10 @@ public class SendEmailOtpRequestValidatorTests // Arrange var otpTokenProvider = Substitute.For>(); var mailService = Substitute.For(); + var featureService = Substitute.For(); // Act - var validator = new SendEmailOtpRequestValidator(otpTokenProvider, mailService); + var validator = new SendEmailOtpRequestValidator(featureService, otpTokenProvider, mailService); // Assert Assert.NotNull(validator); From 76de64263c94c8331586aa898e6462afb214e5b8 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 22 Oct 2025 16:19:43 -0700 Subject: [PATCH 47/52] [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 --- .../Vault/Controllers/CiphersController.cs | 36 ++- .../Models/Request/AttachmentRequestModel.cs | 5 + src/Core/Vault/Services/ICipherService.cs | 8 +- .../Services/Implementations/CipherService.cs | 20 +- .../Vault/Services/CipherServiceTests.cs | 236 ++++++++++++++++++ 5 files changed, 288 insertions(+), 17 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index fe3069d8c7..46d8332926 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using System.Globalization; using System.Text.Json; using Azure.Messaging.EventGrid; using Bit.Api.Auth.Models.Request.Accounts; @@ -1366,7 +1367,7 @@ public class CiphersController : Controller } var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher, - request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id); + request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate); return new AttachmentUploadDataResponseModel { AttachmentId = attachmentId, @@ -1419,9 +1420,11 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Extract lastKnownRevisionDate from form data if present + DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); await Request.GetFileAsync(async (stream) => { - await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData); + await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, lastKnownRevisionDate); }); } @@ -1440,10 +1443,12 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Extract lastKnownRevisionDate from form data if present + DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); await Request.GetFileAsync(async (stream, fileName, key) => { await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, - Request.ContentLength.GetValueOrDefault(0), user.Id); + Request.ContentLength.GetValueOrDefault(0), user.Id, false, lastKnownRevisionDate); }); return new CipherResponseModel( @@ -1469,10 +1474,13 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Extract lastKnownRevisionDate from form data if present + DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); + await Request.GetFileAsync(async (stream, fileName, key) => { await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, - Request.ContentLength.GetValueOrDefault(0), userId, true); + Request.ContentLength.GetValueOrDefault(0), userId, true, lastKnownRevisionDate); }); return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); @@ -1515,10 +1523,13 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Extract lastKnownRevisionDate from form data if present + DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm(); + await Request.GetFileAsync(async (stream, fileName, key) => { await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key, - Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId); + Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate); }); } @@ -1630,4 +1641,19 @@ public class CiphersController : Controller { return await _cipherRepository.GetByIdAsync(cipherId, userId); } + + private DateTime? GetLastKnownRevisionDateFromForm() + { + DateTime? lastKnownRevisionDate = null; + if (Request.Form.TryGetValue("lastKnownRevisionDate", out var dateValue)) + { + if (!DateTime.TryParse(dateValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedDate)) + { + throw new BadRequestException("Invalid lastKnownRevisionDate format."); + } + lastKnownRevisionDate = parsedDate; + } + + return lastKnownRevisionDate; + } } diff --git a/src/Api/Vault/Models/Request/AttachmentRequestModel.cs b/src/Api/Vault/Models/Request/AttachmentRequestModel.cs index 96c66c6044..eef70bf4e4 100644 --- a/src/Api/Vault/Models/Request/AttachmentRequestModel.cs +++ b/src/Api/Vault/Models/Request/AttachmentRequestModel.cs @@ -9,4 +9,9 @@ public class AttachmentRequestModel public string FileName { get; set; } public long FileSize { get; set; } public bool AdminRequest { get; set; } = false; + + /// + /// The last known revision date of the Cipher that this attachment belongs to. + /// + public DateTime? LastKnownRevisionDate { get; set; } } diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index ffd79e9381..110d4b6ea4 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -13,11 +13,11 @@ public interface ICipherService Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, IEnumerable collectionIds = null, bool skipPermissionCheck = false); Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher, - string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId); + string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null); Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, - long requestLength, Guid savingUserId, bool orgAdmin = false); + long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null); Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength, - string attachmentId, Guid organizationShareId); + string attachmentId, Guid organizationShareId, DateTime? lastKnownRevisionDate = null); Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); Task DeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false); @@ -34,7 +34,7 @@ public interface ICipherService Task SoftDeleteManyAsync(IEnumerable cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false); Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false); Task> RestoreManyAsync(IEnumerable cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false); - Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId); + Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, DateTime? lastKnownRevisionDate = null); Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); Task ValidateBulkCollectionAssignmentAsync(IEnumerable collectionIds, IEnumerable cipherIds, Guid userId); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index f132588e37..db458a523d 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -113,7 +113,7 @@ public class CipherService : ICipherService } else { - ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); cipher.RevisionDate = DateTime.UtcNow; await _cipherRepository.ReplaceAsync(cipher); await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated); @@ -168,7 +168,7 @@ public class CipherService : ICipherService } else { - ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); cipher.RevisionDate = DateTime.UtcNow; await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId); await ValidateViewPasswordUserAsync(cipher); @@ -180,8 +180,9 @@ public class CipherService : ICipherService } } - public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment) + public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, DateTime? lastKnownRevisionDate = null) { + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); if (attachment == null) { throw new BadRequestException("Cipher attachment does not exist"); @@ -196,8 +197,9 @@ public class CipherService : ICipherService } public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher, - string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId) + string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null) { + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, adminRequest, fileSize); var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); @@ -232,8 +234,9 @@ public class CipherService : ICipherService } public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key, - long requestLength, Guid savingUserId, bool orgAdmin = false) + long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null) { + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength); var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); @@ -284,10 +287,11 @@ public class CipherService : ICipherService } public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, - long requestLength, string attachmentId, Guid organizationId) + long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null) { try { + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); if (requestLength < 1) { throw new BadRequestException("No data to attach."); @@ -859,7 +863,7 @@ public class CipherService : ICipherService return NormalCipherPermissions.CanRestore(user, cipher, organizationAbility); } - private void ValidateCipherLastKnownRevisionDateAsync(Cipher cipher, DateTime? lastKnownRevisionDate) + private void ValidateCipherLastKnownRevisionDate(Cipher cipher, DateTime? lastKnownRevisionDate) { if (cipher.Id == default || !lastKnownRevisionDate.HasValue) { @@ -1007,7 +1011,7 @@ public class CipherService : ICipherService throw new BadRequestException("Not enough storage available for this organization."); } - ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); + ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate); } private async Task ValidateViewPasswordUserAsync(Cipher cipher) diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 55db5a9143..95391f1f44 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -113,6 +113,242 @@ public class CipherServiceTests await sutProvider.GetDependency().Received(1).ReplaceAsync(cipherDetails); } + [Theory, BitAutoData] + public async Task CreateAttachmentAsync_WrongRevisionDate_Throws(SutProvider sutProvider, Cipher cipher, Guid savingUserId) + { + var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); + var stream = new MemoryStream(); + var fileName = "test.txt"; + var key = "test-key"; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, lastKnownRevisionDate)); + Assert.Contains("out of date", exception.Message); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("Correct Time")] + public async Task CreateAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, + SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var stream = new MemoryStream(new byte[100]); + var fileName = "test.txt"; + var key = "test-key"; + + // Setup cipher with user ownership + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + // Mock user storage and premium access + var user = new User { Id = savingUserId, MaxStorageGb = 1 }; + sutProvider.GetDependency() + .GetByIdAsync(savingUserId) + .Returns(user); + + sutProvider.GetDependency() + .CanAccessPremium(user) + .Returns(true); + + sutProvider.GetDependency() + .UploadNewAttachmentAsync(Arg.Any(), cipher, Arg.Any()) + .Returns(Task.CompletedTask); + + sutProvider.GetDependency() + .ValidateFileAsync(cipher, Arg.Any(), Arg.Any()) + .Returns((true, 100L)); + + sutProvider.GetDependency() + .UpdateAttachmentAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + sutProvider.GetDependency() + .ReplaceAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, lastKnownRevisionDate); + + await sutProvider.GetDependency().Received(1) + .UploadNewAttachmentAsync(Arg.Any(), cipher, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateAttachmentForDelayedUploadAsync_WrongRevisionDate_Throws(SutProvider sutProvider, Cipher cipher, Guid savingUserId) + { + var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); + var key = "test-key"; + var fileName = "test.txt"; + var fileSize = 100L; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAttachmentForDelayedUploadAsync(cipher, key, fileName, fileSize, false, savingUserId, lastKnownRevisionDate)); + Assert.Contains("out of date", exception.Message); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("Correct Time")] + public async Task CreateAttachmentForDelayedUploadAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, + SutProvider sutProvider, CipherDetails cipher, Guid savingUserId) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var key = "test-key"; + var fileName = "test.txt"; + var fileSize = 100L; + + // Setup cipher with user ownership + cipher.UserId = savingUserId; + cipher.OrganizationId = null; + + // Mock user storage and premium access + var user = new User { Id = savingUserId, MaxStorageGb = 1 }; + sutProvider.GetDependency() + .GetByIdAsync(savingUserId) + .Returns(user); + + sutProvider.GetDependency() + .CanAccessPremium(user) + .Returns(true); + + sutProvider.GetDependency() + .GetAttachmentUploadUrlAsync(cipher, Arg.Any()) + .Returns("https://example.com/upload"); + + sutProvider.GetDependency() + .UpdateAttachmentAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + var result = await sutProvider.Sut.CreateAttachmentForDelayedUploadAsync(cipher, key, fileName, fileSize, false, savingUserId, lastKnownRevisionDate); + + Assert.NotNull(result.attachmentId); + Assert.NotNull(result.uploadUrl); + } + + [Theory, BitAutoData] + public async Task UploadFileForExistingAttachmentAsync_WrongRevisionDate_Throws(SutProvider sutProvider, + Cipher cipher) + { + var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); + var stream = new MemoryStream(); + var attachment = new CipherAttachment.MetaData + { + AttachmentId = "test-attachment-id", + Size = 100, + FileName = "test.txt", + Key = "test-key" + }; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate)); + Assert.Contains("out of date", exception.Message); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("Correct Time")] + public async Task UploadFileForExistingAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, + SutProvider sutProvider, CipherDetails cipher) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var stream = new MemoryStream(new byte[100]); + var attachmentId = "test-attachment-id"; + var attachment = new CipherAttachment.MetaData + { + AttachmentId = attachmentId, + Size = 100, + FileName = "test.txt", + Key = "test-key" + }; + + // Set the attachment on the cipher so ValidateCipherAttachmentFile can find it + cipher.SetAttachments(new Dictionary + { + [attachmentId] = attachment + }); + + sutProvider.GetDependency() + .UploadNewAttachmentAsync(stream, cipher, attachment) + .Returns(Task.CompletedTask); + + sutProvider.GetDependency() + .ValidateFileAsync(cipher, attachment, Arg.Any()) + .Returns((true, 100L)); + + sutProvider.GetDependency() + .UpdateAttachmentAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + await sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate); + + await sutProvider.GetDependency().Received(1) + .UploadNewAttachmentAsync(stream, cipher, attachment); + } + + [Theory, BitAutoData] + public async Task CreateAttachmentShareAsync_WrongRevisionDate_Throws(SutProvider sutProvider, + Cipher cipher, Guid organizationId) + { + var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1); + var stream = new MemoryStream(); + var fileName = "test.txt"; + var key = "test-key"; + var attachmentId = "attachment-id"; + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate)); + Assert.Contains("out of date", exception.Message); + } + + [Theory] + [BitAutoData("")] + [BitAutoData("Correct Time")] + public async Task CreateAttachmentShareAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString, + SutProvider sutProvider, CipherDetails cipher, Guid organizationId) + { + var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate; + var stream = new MemoryStream(new byte[100]); + var fileName = "test.txt"; + var key = "test-key"; + var attachmentId = "attachment-id"; + + // Setup cipher with existing attachment (no TempMetadata) + cipher.OrganizationId = null; + cipher.SetAttachments(new Dictionary + { + [attachmentId] = new CipherAttachment.MetaData + { + AttachmentId = attachmentId, + Size = 100, + FileName = "existing.txt", + Key = "existing-key" + } + }); + + // Mock organization + var organization = new Organization + { + Id = organizationId, + MaxStorageGb = 1 + }; + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns(organization); + + sutProvider.GetDependency() + .UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any()) + .Returns(Task.CompletedTask); + + sutProvider.GetDependency() + .UpdateAttachmentAsync(Arg.Any()) + .Returns(Task.CompletedTask); + + await sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate); + + await sutProvider.GetDependency().Received(1) + .UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any()); + } + [Theory] [BitAutoData] public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyEnabled_Throws( From 69f0464e05866bf8425e1aa063be35998decd9ba Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 23 Oct 2025 08:08:09 -0400 Subject: [PATCH 48/52] 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 --- .../EventIntegrations/IIntegrationMessage.cs | 1 + .../EventIntegrations/IntegrationMessage.cs | 1 + .../Services/IEventIntegrationPublisher.cs | 2 +- .../AzureServiceBusService.cs | 11 +++++++---- .../EventIntegrationEventWriteService.cs | 14 ++++++++++---- .../EventIntegrationHandler.cs | 1 + .../EventIntegrations/RabbitMqService.cs | 2 +- .../IntegrationMessageTests.cs | 6 ++++++ .../EventIntegrationEventWriteServiceTests.cs | 8 +++++--- .../Services/EventIntegrationHandlerTests.cs | 18 +++++++++++++----- .../Services/IntegrationHandlerTests.cs | 3 +++ 11 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs index 7a0962d89a..5b6bfe2e53 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs @@ -6,6 +6,7 @@ public interface IIntegrationMessage { IntegrationType IntegrationType { get; } string MessageId { get; set; } + string? OrganizationId { get; set; } int RetryCount { get; } DateTime? DelayUntilDate { get; } void ApplyRetry(DateTime? handlerDelayUntilDate); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs index 11a5229f8c..b0fc2161ba 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs @@ -7,6 +7,7 @@ public class IntegrationMessage : IIntegrationMessage { public IntegrationType IntegrationType { get; set; } public required string MessageId { get; set; } + public string? OrganizationId { get; set; } public required string RenderedTemplate { get; set; } public int RetryCount { get; set; } = 0; public DateTime? DelayUntilDate { get; set; } diff --git a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs index b80b518223..4d95707e90 100644 --- a/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs +++ b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs @@ -5,5 +5,5 @@ namespace Bit.Core.Services; public interface IEventIntegrationPublisher : IAsyncDisposable { Task PublishAsync(IIntegrationMessage message); - Task PublishEventAsync(string body); + Task PublishEventAsync(string body, string? organizationId); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs index 4887aa3a7f..953a9bb56e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusService.cs @@ -30,7 +30,8 @@ public class AzureServiceBusService : IAzureServiceBusService var serviceBusMessage = new ServiceBusMessage(json) { Subject = message.IntegrationType.ToRoutingKey(), - MessageId = message.MessageId + MessageId = message.MessageId, + PartitionKey = message.OrganizationId }; await _integrationSender.SendMessageAsync(serviceBusMessage); @@ -44,18 +45,20 @@ public class AzureServiceBusService : IAzureServiceBusService { Subject = message.IntegrationType.ToRoutingKey(), ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow, - MessageId = message.MessageId + MessageId = message.MessageId, + PartitionKey = message.OrganizationId }; await _integrationSender.SendMessageAsync(serviceBusMessage); } - public async Task PublishEventAsync(string body) + public async Task PublishEventAsync(string body, string? organizationId) { var message = new ServiceBusMessage(body) { ContentType = "application/json", - MessageId = Guid.NewGuid().ToString() + MessageId = Guid.NewGuid().ToString(), + PartitionKey = organizationId }; await _eventSender.SendMessageAsync(message); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs index 309b4a8409..4ac97df763 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs @@ -14,15 +14,21 @@ public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDispo public async Task CreateAsync(IEvent e) { var body = JsonSerializer.Serialize(e); - await _eventIntegrationPublisher.PublishEventAsync(body: body); + await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: e.OrganizationId?.ToString()); } public async Task CreateManyAsync(IEnumerable events) { - var body = JsonSerializer.Serialize(events); - await _eventIntegrationPublisher.PublishEventAsync(body: body); - } + var eventList = events as IList ?? events.ToList(); + if (eventList.Count == 0) + { + return; + } + var organizationId = eventList[0].OrganizationId?.ToString(); + var body = JsonSerializer.Serialize(eventList); + await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: organizationId); + } public async ValueTask DisposeAsync() { await _eventIntegrationPublisher.DisposeAsync(); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 0a8ab67554..8423652eb8 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -57,6 +57,7 @@ public class EventIntegrationHandler( { IntegrationType = integrationType, MessageId = messageId.ToString(), + OrganizationId = organizationId.ToString(), Configuration = config, RenderedTemplate = renderedTemplate, RetryCount = 0, diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs index 3e20e34200..8976530cf4 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs @@ -122,7 +122,7 @@ public class RabbitMqService : IRabbitMqService body: body); } - public async Task PublishEventAsync(string body) + public async Task PublishEventAsync(string body, string? organizationId) { await using var channel = await CreateChannelAsync(); var properties = new BasicProperties diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs index edd5cd488f..71f9a15037 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Test.Models.Data.EventIntegrations; public class IntegrationMessageTests { private const string _messageId = "TestMessageId"; + private const string _organizationId = "TestOrganizationId"; [Fact] public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate() @@ -16,6 +17,7 @@ public class IntegrationMessageTests { Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = _messageId, + OrganizationId = _organizationId, RetryCount = 2, RenderedTemplate = string.Empty, DelayUntilDate = null @@ -36,6 +38,7 @@ public class IntegrationMessageTests { Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = _messageId, + OrganizationId = _organizationId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, RetryCount = 2, @@ -48,6 +51,7 @@ public class IntegrationMessageTests Assert.NotNull(result); Assert.Equal(message.Configuration, result.Configuration); Assert.Equal(message.MessageId, result.MessageId); + Assert.Equal(message.OrganizationId, result.OrganizationId); Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); Assert.Equal(message.IntegrationType, result.IntegrationType); Assert.Equal(message.RetryCount, result.RetryCount); @@ -67,6 +71,7 @@ public class IntegrationMessageTests var message = new IntegrationMessage { MessageId = _messageId, + OrganizationId = _organizationId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, RetryCount = 2, @@ -77,6 +82,7 @@ public class IntegrationMessageTests var result = JsonSerializer.Deserialize(json); Assert.Equal(message.MessageId, result.MessageId); + Assert.Equal(message.OrganizationId, result.OrganizationId); Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); Assert.Equal(message.IntegrationType, result.IntegrationType); Assert.Equal(message.RetryCount, result.RetryCount); diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs index 9369690d86..03f9c7764d 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs @@ -22,18 +22,20 @@ public class EventIntegrationEventWriteServiceTests [Theory, BitAutoData] public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage) { - var expected = JsonSerializer.Serialize(eventMessage); await Subject.CreateAsync(eventMessage); await _eventIntegrationPublisher.Received(1).PublishEventAsync( - Arg.Is(body => AssertJsonStringsMatch(eventMessage, body))); + body: Arg.Is(body => AssertJsonStringsMatch(eventMessage, body)), + organizationId: Arg.Is(orgId => eventMessage.OrganizationId.ToString().Equals(orgId))); } [Theory, BitAutoData] public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable eventMessages) { + var eventMessage = eventMessages.First(); await Subject.CreateManyAsync(eventMessages); await _eventIntegrationPublisher.Received(1).PublishEventAsync( - Arg.Is(body => AssertJsonStringsMatch(eventMessages, body))); + body: Arg.Is(body => AssertJsonStringsMatch(eventMessages, body)), + organizationId: Arg.Is(orgId => eventMessage.OrganizationId.ToString().Equals(orgId))); } private static bool AssertJsonStringsMatch(EventMessage expected, string body) diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index f038fe28ef..89207a9d3a 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -23,6 +23,7 @@ public class EventIntegrationHandlerTests private const string _templateWithOrganization = "Org: #OrganizationName#"; private const string _templateWithUser = "#UserName#, #UserEmail#"; private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private static readonly Guid _organizationId = Guid.NewGuid(); private static readonly Uri _uri = new Uri("https://localhost"); private static readonly Uri _uri2 = new Uri("https://example.com"); private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); @@ -50,6 +51,7 @@ public class EventIntegrationHandlerTests { IntegrationType = IntegrationType.Webhook, MessageId = "TestMessageId", + OrganizationId = _organizationId.ToString(), Configuration = new WebhookIntegrationConfigurationDetails(_uri), RenderedTemplate = template, RetryCount = 0, @@ -122,6 +124,7 @@ public class EventIntegrationHandlerTests public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + eventMessage.OrganizationId = _organizationId; await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -140,6 +143,7 @@ public class EventIntegrationHandlerTests public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage) { var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); + eventMessage.OrganizationId = _organizationId; await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -164,6 +168,7 @@ public class EventIntegrationHandlerTests var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; + eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -183,6 +188,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); var organization = Substitute.For(); organization.Name = "Test"; + eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -205,6 +211,7 @@ public class EventIntegrationHandlerTests var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; + eventMessage.OrganizationId = _organizationId; sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -235,6 +242,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(ValidFilterConfiguration()); sutProvider.GetDependency().EvaluateFilterGroup( Arg.Any(), Arg.Any()).Returns(true); + eventMessage.OrganizationId = _organizationId; await sutProvider.Sut.HandleEventAsync(eventMessage); @@ -284,7 +292,7 @@ public class EventIntegrationHandlerTests $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId", "OrganizationId" }))); } } @@ -301,12 +309,12 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( + expectedMessage, new[] { "MessageId", "OrganizationId" }))); expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2); - await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( - AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( + expectedMessage, new[] { "MessageId", "OrganizationId" }))); } } } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index aa93567538..f6f587cfd7 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -16,6 +16,7 @@ public class IntegrationHandlerTests { Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = "TestMessageId", + OrganizationId = "TestOrganizationId", IntegrationType = IntegrationType.Webhook, RenderedTemplate = "Template", DelayUntilDate = null, @@ -25,6 +26,8 @@ public class IntegrationHandlerTests var result = await sut.HandleAsync(expected.ToJson()); var typedResult = Assert.IsType>(result.Message); + Assert.Equal(expected.MessageId, typedResult.MessageId); + Assert.Equal(expected.OrganizationId, typedResult.OrganizationId); Assert.Equal(expected.Configuration, typedResult.Configuration); Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate); Assert.Equal(expected.IntegrationType, typedResult.IntegrationType); From dd1f0a120a393624b05f4467a196a464f4adb6f7 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:40:57 +0200 Subject: [PATCH 49/52] Notifications service unit test coverage with small refactor (#6126) --- src/Notifications/AzureQueueHostedService.cs | 44 +-- .../Controllers/SendController.cs | 28 +- src/Notifications/HubHelpers.cs | 124 ++++++--- src/Notifications/Startup.cs | 1 + test/Notifications.Test/HubHelpersTest.cs | 250 ++++++++++++++++++ .../Notifications.Test.csproj | 2 + 6 files changed, 380 insertions(+), 69 deletions(-) create mode 100644 test/Notifications.Test/HubHelpersTest.cs diff --git a/src/Notifications/AzureQueueHostedService.cs b/src/Notifications/AzureQueueHostedService.cs index 94aa14eaf6..40dd8d22d4 100644 --- a/src/Notifications/AzureQueueHostedService.cs +++ b/src/Notifications/AzureQueueHostedService.cs @@ -1,34 +1,26 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Azure.Storage.Queues; +using Azure.Storage.Queues; using Bit.Core.Settings; using Bit.Core.Utilities; -using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; public class AzureQueueHostedService : IHostedService, IDisposable { private readonly ILogger _logger; - private readonly IHubContext _hubContext; - private readonly IHubContext _anonymousHubContext; + private readonly HubHelpers _hubHelpers; private readonly GlobalSettings _globalSettings; - private Task _executingTask; - private CancellationTokenSource _cts; - private QueueClient _queueClient; + private Task? _executingTask; + private CancellationTokenSource? _cts; public AzureQueueHostedService( ILogger logger, - IHubContext hubContext, - IHubContext anonymousHubContext, + HubHelpers hubHelpers, GlobalSettings globalSettings) { _logger = logger; - _hubContext = hubContext; + _hubHelpers = hubHelpers; _globalSettings = globalSettings; - _anonymousHubContext = anonymousHubContext; } public Task StartAsync(CancellationToken cancellationToken) @@ -44,32 +36,39 @@ public class AzureQueueHostedService : IHostedService, IDisposable { return; } + _logger.LogWarning("Stopping service."); - _cts.Cancel(); + _cts?.Cancel(); await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken)); cancellationToken.ThrowIfCancellationRequested(); } public void Dispose() - { } + { + } private async Task ExecuteAsync(CancellationToken cancellationToken) { - _queueClient = new QueueClient(_globalSettings.Notifications.ConnectionString, "notifications"); + var queueClient = new QueueClient(_globalSettings.Notifications.ConnectionString, "notifications"); while (!cancellationToken.IsCancellationRequested) { try { - var messages = await _queueClient.ReceiveMessagesAsync(32); + var messages = await queueClient.ReceiveMessagesAsync(32, cancellationToken: cancellationToken); if (messages.Value?.Any() ?? false) { foreach (var message in messages.Value) { try { - await HubHelpers.SendNotificationToHubAsync( - message.DecodeMessageText(), _hubContext, _anonymousHubContext, _logger, cancellationToken); - await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt); + var decodedMessage = message.DecodeMessageText(); + if (!string.IsNullOrWhiteSpace(decodedMessage)) + { + await _hubHelpers.SendNotificationToHubAsync(decodedMessage, cancellationToken); + } + + await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt, + cancellationToken); } catch (Exception e) { @@ -77,7 +76,8 @@ public class AzureQueueHostedService : IHostedService, IDisposable message.MessageId, message.DequeueCount); if (message.DequeueCount > 2) { - await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt); + await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt, + cancellationToken); } } } diff --git a/src/Notifications/Controllers/SendController.cs b/src/Notifications/Controllers/SendController.cs index 7debd51df7..c663102b56 100644 --- a/src/Notifications/Controllers/SendController.cs +++ b/src/Notifications/Controllers/SendController.cs @@ -1,36 +1,30 @@ -using System.Text; +#nullable enable +using System.Text; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -namespace Bit.Notifications; +namespace Bit.Notifications.Controllers; [Authorize("Internal")] public class SendController : Controller { - private readonly IHubContext _hubContext; - private readonly IHubContext _anonymousHubContext; - private readonly ILogger _logger; + private readonly HubHelpers _hubHelpers; - public SendController(IHubContext hubContext, IHubContext anonymousHubContext, ILogger logger) + public SendController(HubHelpers hubHelpers) { - _hubContext = hubContext; - _anonymousHubContext = anonymousHubContext; - _logger = logger; + _hubHelpers = hubHelpers; } [HttpPost("~/send")] [SelfHosted(SelfHostedOnly = true)] - public async Task PostSend() + public async Task PostSendAsync() { - using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) + using var reader = new StreamReader(Request.Body, Encoding.UTF8); + var notificationJson = await reader.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(notificationJson)) { - var notificationJson = await reader.ReadToEndAsync(); - if (!string.IsNullOrWhiteSpace(notificationJson)) - { - await HubHelpers.SendNotificationToHubAsync(notificationJson, _hubContext, _anonymousHubContext, _logger); - } + await _hubHelpers.SendNotificationToHubAsync(notificationJson); } } } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 0fea72edc3..2ef674adfe 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -1,31 +1,39 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; -public static class HubHelpers +public class HubHelpers { - private static JsonSerializerOptions _deserializerOptions = - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions _deserializerOptions = new() { PropertyNameCaseInsensitive = true }; private static readonly string _receiveMessageMethod = "ReceiveMessage"; - public static async Task SendNotificationToHubAsync( - string notificationJson, - IHubContext hubContext, + private readonly IHubContext _hubContext; + private readonly IHubContext _anonymousHubContext; + private readonly ILogger _logger; + + public HubHelpers(IHubContext hubContext, IHubContext anonymousHubContext, - ILogger logger, - CancellationToken cancellationToken = default(CancellationToken) - ) + ILogger logger) + { + _hubContext = hubContext; + _anonymousHubContext = anonymousHubContext; + _logger = logger; + } + + public async Task SendNotificationToHubAsync(string notificationJson, CancellationToken cancellationToken = default) { var notification = JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); - logger.LogInformation("Sending notification: {NotificationType}", notification.Type); + if (notification is null) + { + return; + } + + _logger.LogInformation("Sending notification: {NotificationType}", notification.Type); switch (notification.Type) { case PushType.SyncCipherUpdate: @@ -35,14 +43,19 @@ public static class HubHelpers var cipherNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); + if (cipherNotification is null) + { + break; + } + if (cipherNotification.Payload.UserId.HasValue) { - await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString()) + await _hubContext.Clients.User(cipherNotification.Payload.UserId.Value.ToString()) .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken); } else if (cipherNotification.Payload.OrganizationId.HasValue) { - await hubContext.Clients + await _hubContext.Clients .Group(NotificationsHub.GetOrganizationGroup(cipherNotification.Payload.OrganizationId.Value)) .SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken); } @@ -54,7 +67,12 @@ public static class HubHelpers var folderNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.User(folderNotification.Payload.UserId.ToString()) + if (folderNotification is null) + { + break; + } + + await _hubContext.Clients.User(folderNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, folderNotification, cancellationToken); break; case PushType.SyncCiphers: @@ -66,7 +84,12 @@ public static class HubHelpers var userNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.User(userNotification.Payload.UserId.ToString()) + if (userNotification is null) + { + break; + } + + await _hubContext.Clients.User(userNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, userNotification, cancellationToken); break; case PushType.SyncSendCreate: @@ -75,36 +98,65 @@ public static class HubHelpers var sendNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.User(sendNotification.Payload.UserId.ToString()) + if (sendNotification is null) + { + break; + } + + await _hubContext.Clients.User(sendNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, sendNotification, cancellationToken); break; case PushType.AuthRequestResponse: var authRequestResponseNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString()) + if (authRequestResponseNotification is null) + { + break; + } + + await _anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString()) .SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken); break; case PushType.AuthRequest: var authRequestNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString()) + if (authRequestNotification is null) + { + break; + } + + await _hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, authRequestNotification, cancellationToken); break; case PushType.SyncOrganizationStatusChanged: var orgStatusNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId)) + if (orgStatusNotification is null) + { + break; + } + + await _hubContext.Clients + .Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId)) .SendAsync(_receiveMessageMethod, orgStatusNotification, cancellationToken); break; case PushType.SyncOrganizationCollectionSettingChanged: var organizationCollectionSettingsChangedNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId)) - .SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken); + if (organizationCollectionSettingsChangedNotification is null) + { + break; + } + + await _hubContext.Clients + .Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification + .Payload.OrganizationId)) + .SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, + cancellationToken); break; case PushType.OrganizationBankAccountVerified: var organizationBankAccountVerifiedNotification = @@ -124,9 +176,14 @@ public static class HubHelpers case PushType.NotificationStatus: var notificationData = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); + if (notificationData is null) + { + break; + } + if (notificationData.Payload.InstallationId.HasValue) { - await hubContext.Clients.Group(NotificationsHub.GetInstallationGroup( + await _hubContext.Clients.Group(NotificationsHub.GetInstallationGroup( notificationData.Payload.InstallationId.Value, notificationData.Payload.ClientType)) .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } @@ -134,27 +191,34 @@ public static class HubHelpers { if (notificationData.Payload.ClientType == ClientType.All) { - await hubContext.Clients.User(notificationData.Payload.UserId.ToString()) + await _hubContext.Clients.User(notificationData.Payload.UserId.Value.ToString()) .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } else { - await hubContext.Clients.Group(NotificationsHub.GetUserGroup( + await _hubContext.Clients.Group(NotificationsHub.GetUserGroup( notificationData.Payload.UserId.Value, notificationData.Payload.ClientType)) .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } } else if (notificationData.Payload.OrganizationId.HasValue) { - await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup( + await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup( notificationData.Payload.OrganizationId.Value, notificationData.Payload.ClientType)) .SendAsync(_receiveMessageMethod, notificationData, cancellationToken); } break; case PushType.RefreshSecurityTasks: - var pendingTasksData = JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); - await hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString()) + var pendingTasksData = + JsonSerializer.Deserialize>(notificationJson, + _deserializerOptions); + if (pendingTasksData is null) + { + break; + } + + await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken); break; default: diff --git a/src/Notifications/Startup.cs b/src/Notifications/Startup.cs index eb3c3f8682..2889e90d3b 100644 --- a/src/Notifications/Startup.cs +++ b/src/Notifications/Startup.cs @@ -61,6 +61,7 @@ public class Startup } services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Mvc services.AddMvc(); diff --git a/test/Notifications.Test/HubHelpersTest.cs b/test/Notifications.Test/HubHelpersTest.cs new file mode 100644 index 0000000000..df4d3c5f85 --- /dev/null +++ b/test/Notifications.Test/HubHelpersTest.cs @@ -0,0 +1,250 @@ +#nullable enable +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Models; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Core.Utilities; +using Bit.Notifications; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.SignalR; +using NSubstitute; + +namespace Notifications.Test; + +[SutProviderCustomize] +[NotificationCustomize(false)] +public class HubHelpersTest +{ + [Theory] + [BitAutoData] + public async Task SendNotificationToHubAsync_NotificationPushNotificationGlobal_NothingSent( + SutProvider sutProvider, + NotificationPushNotification notification, + string contextId, CancellationToken cancellationToke) + { + notification.Global = true; + notification.InstallationId = null; + notification.UserId = null; + notification.OrganizationId = null; + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToke); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0).Group(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task + SendNotificationToHubAsync_NotificationPushNotificationInstallationIdProvidedClientTypeAll_SentToGroupInstallation( + SutProvider sutProvider, + NotificationPushNotification notification, + string contextId, CancellationToken cancellationToken) + { + notification.UserId = null; + notification.OrganizationId = null; + notification.ClientType = ClientType.All; + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + await sutProvider.GetDependency>().Clients.Received(1) + .Group($"Installation_{notification.InstallationId!.Value.ToString()}") + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async Task + SendNotificationToHubAsync_NotificationPushNotificationInstallationIdProvidedClientTypeNotAll_SentToGroupInstallationClientType( + ClientType clientType, SutProvider sutProvider, + NotificationPushNotification notification, + string contextId, CancellationToken cancellationToken) + { + notification.UserId = null; + notification.OrganizationId = null; + notification.ClientType = clientType; + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + await sutProvider.GetDependency>().Clients.Received(1) + .Group($"Installation_ClientType_{notification.InstallationId!.Value}_{clientType}") + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + public async Task SendNotificationToHubAsync_NotificationPushNotificationUserIdProvidedClientTypeAll_SentToUser( + bool organizationIdProvided, SutProvider sutProvider, + NotificationPushNotification notification, + string contextId, CancellationToken cancellationToken) + { + notification.InstallationId = null; + notification.ClientType = ClientType.All; + if (!organizationIdProvided) + { + notification.OrganizationId = null; + } + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + await sutProvider.GetDependency>().Clients.Received(1) + .User(notification.UserId!.Value.ToString()) + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).Group(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData(false, ClientType.Browser)] + [BitAutoData(false, ClientType.Desktop)] + [BitAutoData(false, ClientType.Mobile)] + [BitAutoData(false, ClientType.Web)] + [BitAutoData(true, ClientType.Browser)] + [BitAutoData(true, ClientType.Desktop)] + [BitAutoData(true, ClientType.Mobile)] + [BitAutoData(true, ClientType.Web)] + public async Task + SendNotificationToHubAsync_NotificationPushNotificationUserIdProvidedClientTypeNotAll_SentToGroupUserClientType( + bool organizationIdProvided, ClientType clientType, SutProvider sutProvider, + NotificationPushNotification notification, + string contextId, CancellationToken cancellationToken) + { + notification.InstallationId = null; + notification.ClientType = clientType; + if (!organizationIdProvided) + { + notification.OrganizationId = null; + } + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + await sutProvider.GetDependency>().Clients.Received(1) + .Group($"UserClientType_{notification.UserId!.Value}_{clientType}") + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task + SendNotificationToHubAsync_NotificationPushNotificationOrganizationIdProvidedClientTypeAll_SentToGroupOrganization( + SutProvider sutProvider, string contextId, + NotificationPushNotification notification, + CancellationToken cancellationToken) + { + notification.UserId = null; + notification.InstallationId = null; + notification.ClientType = ClientType.All; + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + await sutProvider.GetDependency>().Clients.Received(1) + .Group($"Organization_{notification.OrganizationId!.Value}") + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async Task + SendNotificationToHubAsync_NotificationPushNotificationOrganizationIdProvidedClientTypeNotAll_SentToGroupOrganizationClientType( + ClientType clientType, SutProvider sutProvider, string contextId, + NotificationPushNotification notification, + CancellationToken cancellationToken) + { + notification.UserId = null; + notification.InstallationId = null; + notification.ClientType = clientType; + + var json = ToNotificationJson(notification, PushType.Notification, contextId); + await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken); + + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + await sutProvider.GetDependency>().Clients.Received(1) + .Group($"OrganizationClientType_{notification.OrganizationId!.Value}_{clientType}") + .Received(1) + .SendCoreAsync("ReceiveMessage", Arg.Is(objects => + objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0], + PushType.Notification, contextId)), + cancellationToken); + sutProvider.GetDependency>().Clients.Received(0).User(Arg.Any()); + sutProvider.GetDependency>().Clients.Received(0) + .Group(Arg.Any()); + } + + private static string ToNotificationJson(object payload, PushType type, string contextId) + { + var notification = new PushNotificationData(type, payload, contextId); + return JsonSerializer.Serialize(notification, JsonHelpers.IgnoreWritingNull); + } + + private static bool IsNotificationPushNotificationEqual(NotificationPushNotification expected, object? actual, + PushType type, string contextId) + { + if (actual is not PushNotificationData pushNotificationData) + { + return false; + } + + return pushNotificationData.Type == type && + pushNotificationData.ContextId == contextId && + expected.Id == pushNotificationData.Payload.Id && + expected.UserId == pushNotificationData.Payload.UserId && + expected.OrganizationId == pushNotificationData.Payload.OrganizationId && + expected.ClientType == pushNotificationData.Payload.ClientType && + expected.RevisionDate == pushNotificationData.Payload.RevisionDate; + } +} diff --git a/test/Notifications.Test/Notifications.Test.csproj b/test/Notifications.Test/Notifications.Test.csproj index 4dd37605c2..a4bab9df98 100644 --- a/test/Notifications.Test/Notifications.Test.csproj +++ b/test/Notifications.Test/Notifications.Test.csproj @@ -18,5 +18,7 @@ + + From b15913ce737473af239afd948b7e201968f25410 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:04:47 -0400 Subject: [PATCH 50/52] Fix HubHelpers field references causing build error (#6487) --- src/Notifications/HubHelpers.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 2ef674adfe..b0dec8b415 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -162,14 +162,24 @@ public class HubHelpers var organizationBankAccountVerifiedNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationBankAccountVerifiedNotification.Payload.OrganizationId)) + if (organizationBankAccountVerifiedNotification is null) + { + break; + } + + await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationBankAccountVerifiedNotification.Payload.OrganizationId)) .SendAsync(_receiveMessageMethod, organizationBankAccountVerifiedNotification, cancellationToken); break; case PushType.ProviderBankAccountVerified: var providerBankAccountVerifiedNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - await hubContext.Clients.User(providerBankAccountVerifiedNotification.Payload.AdminId.ToString()) + if (providerBankAccountVerifiedNotification is null) + { + break; + } + + await _hubContext.Clients.User(providerBankAccountVerifiedNotification.Payload.AdminId.ToString()) .SendAsync(_receiveMessageMethod, providerBankAccountVerifiedNotification, cancellationToken); break; case PushType.Notification: @@ -222,7 +232,7 @@ public class HubHelpers .SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken); break; default: - logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type); + _logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type); break; } } From ff4b3eb9e5896f74aa4499da7378d66a90b314f6 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 23 Oct 2025 14:47:23 -0400 Subject: [PATCH 51/52] [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 --- ...zedPaymentMethodTypeValidationAttribute.cs | 13 ++ ...edPaymentMethodTypeValidationAttribute.cs} | 4 +- .../MinimalTokenizedPaymentMethodRequest.cs | 2 +- .../NonTokenizedPaymentMethodRequest.cs | 21 ++++ .../PremiumCloudHostedSubscriptionRequest.cs | 37 +++++- .../Models/NonTokenizedPaymentMethod.cs | 11 ++ .../Billing/Payment/Models/PaymentMethod.cs | 69 +++++++++++ ...tePremiumCloudHostedSubscriptionCommand.cs | 81 ++++++++----- .../Payment/Models/PaymentMethodTests.cs | 112 ++++++++++++++++++ ...miumCloudHostedSubscriptionCommandTests.cs | 78 +++++++++++- 10 files changed, 391 insertions(+), 37 deletions(-) create mode 100644 src/Api/Billing/Attributes/NonTokenizedPaymentMethodTypeValidationAttribute.cs rename src/Api/Billing/Attributes/{PaymentMethodTypeValidationAttribute.cs => TokenizedPaymentMethodTypeValidationAttribute.cs} (62%) create mode 100644 src/Api/Billing/Models/Requests/Payment/NonTokenizedPaymentMethodRequest.cs create mode 100644 src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs create mode 100644 src/Core/Billing/Payment/Models/PaymentMethod.cs create mode 100644 test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs diff --git a/src/Api/Billing/Attributes/NonTokenizedPaymentMethodTypeValidationAttribute.cs b/src/Api/Billing/Attributes/NonTokenizedPaymentMethodTypeValidationAttribute.cs new file mode 100644 index 0000000000..7a906d4838 --- /dev/null +++ b/src/Api/Billing/Attributes/NonTokenizedPaymentMethodTypeValidationAttribute.cs @@ -0,0 +1,13 @@ +using Bit.Api.Utilities; + +namespace Bit.Api.Billing.Attributes; + +public class NonTokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute +{ + private static readonly string[] _acceptedValues = ["accountCredit"]; + + public NonTokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues) + { + ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}"; + } +} diff --git a/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs b/src/Api/Billing/Attributes/TokenizedPaymentMethodTypeValidationAttribute.cs similarity index 62% rename from src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs rename to src/Api/Billing/Attributes/TokenizedPaymentMethodTypeValidationAttribute.cs index 227b454f9f..51e40e9999 100644 --- a/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs +++ b/src/Api/Billing/Attributes/TokenizedPaymentMethodTypeValidationAttribute.cs @@ -2,11 +2,11 @@ namespace Bit.Api.Billing.Attributes; -public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute +public class TokenizedPaymentMethodTypeValidationAttribute : StringMatchesAttribute { private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"]; - public PaymentMethodTypeValidationAttribute() : base(_acceptedValues) + public TokenizedPaymentMethodTypeValidationAttribute() : base(_acceptedValues) { ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}"; } diff --git a/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs index b0e415c262..1311805ad4 100644 --- a/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs @@ -7,7 +7,7 @@ namespace Bit.Api.Billing.Models.Requests.Payment; public class MinimalTokenizedPaymentMethodRequest { [Required] - [PaymentMethodTypeValidation] + [TokenizedPaymentMethodTypeValidation] public required string Type { get; set; } [Required] diff --git a/src/Api/Billing/Models/Requests/Payment/NonTokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/NonTokenizedPaymentMethodRequest.cs new file mode 100644 index 0000000000..d15bc73778 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/NonTokenizedPaymentMethodRequest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Attributes; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public class NonTokenizedPaymentMethodRequest +{ + [Required] + [NonTokenizedPaymentMethodTypeValidation] + public required string Type { get; set; } + + public NonTokenizedPaymentMethod ToDomain() + { + return Type switch + { + "accountCredit" => new NonTokenizedPaymentMethod { Type = NonTokenizablePaymentMethodType.AccountCredit }, + _ => throw new InvalidOperationException($"Invalid value for {nameof(NonTokenizedPaymentMethod)}.{nameof(NonTokenizedPaymentMethod.Type)}") + }; + } +} diff --git a/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs index 03f20ec9c1..0f9198fdad 100644 --- a/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs +++ b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs @@ -4,10 +4,10 @@ using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Premium; -public class PremiumCloudHostedSubscriptionRequest +public class PremiumCloudHostedSubscriptionRequest : IValidatableObject { - [Required] - public required MinimalTokenizedPaymentMethodRequest TokenizedPaymentMethod { get; set; } + public MinimalTokenizedPaymentMethodRequest? TokenizedPaymentMethod { get; set; } + public NonTokenizedPaymentMethodRequest? NonTokenizedPaymentMethod { get; set; } [Required] public required MinimalBillingAddressRequest BillingAddress { get; set; } @@ -15,11 +15,38 @@ public class PremiumCloudHostedSubscriptionRequest [Range(0, 99)] public short AdditionalStorageGb { get; set; } = 0; - public (TokenizedPaymentMethod, BillingAddress, short) ToDomain() + + public (PaymentMethod, BillingAddress, short) ToDomain() { - var paymentMethod = TokenizedPaymentMethod.ToDomain(); + // Check if TokenizedPaymentMethod or NonTokenizedPaymentMethod is provided. + var tokenizedPaymentMethod = TokenizedPaymentMethod?.ToDomain(); + var nonTokenizedPaymentMethod = NonTokenizedPaymentMethod?.ToDomain(); + + PaymentMethod paymentMethod = tokenizedPaymentMethod != null + ? tokenizedPaymentMethod + : nonTokenizedPaymentMethod!; + var billingAddress = BillingAddress.ToDomain(); return (paymentMethod, billingAddress, AdditionalStorageGb); } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (TokenizedPaymentMethod == null && NonTokenizedPaymentMethod == null) + { + yield return new ValidationResult( + "Either TokenizedPaymentMethod or NonTokenizedPaymentMethod must be provided.", + new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) } + ); + } + + if (TokenizedPaymentMethod != null && NonTokenizedPaymentMethod != null) + { + yield return new ValidationResult( + "Only one of TokenizedPaymentMethod or NonTokenizedPaymentMethod can be provided.", + new[] { nameof(TokenizedPaymentMethod), nameof(NonTokenizedPaymentMethod) } + ); + } + } } diff --git a/src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs b/src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs new file mode 100644 index 0000000000..5e8ec0484c --- /dev/null +++ b/src/Core/Billing/Payment/Models/NonTokenizedPaymentMethod.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Billing.Payment.Models; + +public record NonTokenizedPaymentMethod +{ + public NonTokenizablePaymentMethodType Type { get; set; } +} + +public enum NonTokenizablePaymentMethodType +{ + AccountCredit, +} diff --git a/src/Core/Billing/Payment/Models/PaymentMethod.cs b/src/Core/Billing/Payment/Models/PaymentMethod.cs new file mode 100644 index 0000000000..a6835f9a32 --- /dev/null +++ b/src/Core/Billing/Payment/Models/PaymentMethod.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; + +namespace Bit.Core.Billing.Payment.Models; + +[JsonConverter(typeof(PaymentMethodJsonConverter))] +public class PaymentMethod(OneOf input) + : OneOfBase(input) +{ + public static implicit operator PaymentMethod(TokenizedPaymentMethod tokenized) => new(tokenized); + public static implicit operator PaymentMethod(NonTokenizedPaymentMethod nonTokenized) => new(nonTokenized); + public bool IsTokenized => IsT0; + public bool IsNonTokenized => IsT1; +} + +internal class PaymentMethodJsonConverter : JsonConverter +{ + public override PaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var element = JsonElement.ParseValue(ref reader); + + if (!element.TryGetProperty("type", out var typeProperty)) + { + throw new JsonException("PaymentMethod requires a 'type' property"); + } + + var type = typeProperty.GetString(); + + + if (Enum.TryParse(type, true, out var tokenizedType) && + Enum.IsDefined(typeof(TokenizablePaymentMethodType), tokenizedType)) + { + var token = element.TryGetProperty("token", out var tokenProperty) ? tokenProperty.GetString() : null; + if (string.IsNullOrEmpty(token)) + { + throw new JsonException("TokenizedPaymentMethod requires a 'token' property"); + } + + return new TokenizedPaymentMethod { Type = tokenizedType, Token = token }; + } + + if (Enum.TryParse(type, true, out var nonTokenizedType) && + Enum.IsDefined(typeof(NonTokenizablePaymentMethodType), nonTokenizedType)) + { + return new NonTokenizedPaymentMethod { Type = nonTokenizedType }; + } + + throw new JsonException($"Unknown payment method type: {type}"); + } + + public override void Write(Utf8JsonWriter writer, PaymentMethod value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + value.Switch( + tokenized => + { + writer.WriteString("type", + tokenized.Type.ToString().ToLowerInvariant() + ); + writer.WriteString("token", tokenized.Token); + }, + nonTokenized => { writer.WriteString("type", nonTokenized.Type.ToString().ToLowerInvariant()); } + ); + + writer.WriteEndObject(); + } +} diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs index fa01acabda..3b2ac5343f 100644 --- a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Logging; using OneOf.Types; using Stripe; using Customer = Stripe.Customer; +using PaymentMethod = Bit.Core.Billing.Payment.Models.PaymentMethod; using Subscription = Stripe.Subscription; namespace Bit.Core.Billing.Premium.Commands; @@ -38,7 +39,7 @@ public interface ICreatePremiumCloudHostedSubscriptionCommand /// A billing command result indicating success or failure with appropriate error details. Task> Run( User user, - TokenizedPaymentMethod paymentMethod, + PaymentMethod paymentMethod, BillingAddress billingAddress, short additionalStorageGb); } @@ -60,7 +61,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand( public Task> Run( User user, - TokenizedPaymentMethod paymentMethod, + PaymentMethod paymentMethod, BillingAddress billingAddress, short additionalStorageGb) => HandleAsync(async () => { @@ -74,6 +75,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand( return new BadRequest("Additional storage must be greater than 0."); } + // Note: A customer will already exist if the customer has purchased account credits. var customer = string.IsNullOrEmpty(user.GatewayCustomerId) ? await CreateCustomerAsync(user, paymentMethod, billingAddress) : await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand }); @@ -82,18 +84,31 @@ public class CreatePremiumCloudHostedSubscriptionCommand( var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null); - switch (paymentMethod) - { - case { Type: TokenizablePaymentMethodType.PayPal } - when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete: - case { Type: not TokenizablePaymentMethodType.PayPal } - when subscription.Status == StripeConstants.SubscriptionStatus.Active: + paymentMethod.Switch( + tokenized => + { + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch (tokenized) + { + case { Type: TokenizablePaymentMethodType.PayPal } + when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete: + case { Type: not TokenizablePaymentMethodType.PayPal } + when subscription.Status == StripeConstants.SubscriptionStatus.Active: + { + user.Premium = true; + user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); + break; + } + } + }, + nonTokenized => + { + if (subscription.Status == StripeConstants.SubscriptionStatus.Active) { user.Premium = true; user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd(); - break; } - } + }); user.Gateway = GatewayType.Stripe; user.GatewayCustomerId = customer.Id; @@ -109,9 +124,15 @@ public class CreatePremiumCloudHostedSubscriptionCommand( }); private async Task CreateCustomerAsync(User user, - TokenizedPaymentMethod paymentMethod, + PaymentMethod paymentMethod, BillingAddress billingAddress) { + if (paymentMethod.IsNonTokenized) + { + _logger.LogError("Cannot create customer for user ({UserID}) using non-tokenized payment method. The customer should already exist", user.Id); + throw new BillingException(); + } + var subscriberName = user.SubscriberName(); var customerCreateOptions = new CustomerCreateOptions { @@ -153,13 +174,14 @@ public class CreatePremiumCloudHostedSubscriptionCommand( var braintreeCustomerId = ""; + // We have checked that the payment method is tokenized, so we can safely cast it. // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - switch (paymentMethod.Type) + switch (paymentMethod.AsT0.Type) { case TokenizablePaymentMethodType.BankAccount: { var setupIntent = - (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.Token })) + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.AsT0.Token })) .FirstOrDefault(); if (setupIntent == null) @@ -173,19 +195,19 @@ public class CreatePremiumCloudHostedSubscriptionCommand( } case TokenizablePaymentMethodType.Card: { - customerCreateOptions.PaymentMethod = paymentMethod.Token; - customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token; + customerCreateOptions.PaymentMethod = paymentMethod.AsT0.Token; + customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.AsT0.Token; break; } case TokenizablePaymentMethodType.PayPal: { - braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.Token); + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.AsT0.Token); customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; break; } default: { - _logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.Type.ToString()); + _logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.AsT0.Type.ToString()); throw new BillingException(); } } @@ -203,18 +225,21 @@ public class CreatePremiumCloudHostedSubscriptionCommand( async Task Revert() { // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (paymentMethod.Type) + if (paymentMethod.IsTokenized) { - case TokenizablePaymentMethodType.BankAccount: - { - await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id); - break; - } - case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): - { - await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); - break; - } + switch (paymentMethod.AsT0.Type) + { + case TokenizablePaymentMethodType.BankAccount: + { + await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id); + break; + } + case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } + } } } } diff --git a/test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs b/test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs new file mode 100644 index 0000000000..e3953cd152 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Models/PaymentMethodTests.cs @@ -0,0 +1,112 @@ +using System.Text.Json; +using Bit.Core.Billing.Payment.Models; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Models; + +public class PaymentMethodTests +{ + [Theory] + [InlineData("{\"cardNumber\":\"1234\"}")] + [InlineData("{\"type\":\"unknown_type\",\"data\":\"value\"}")] + [InlineData("{\"type\":\"invalid\",\"token\":\"test-token\"}")] + [InlineData("{\"type\":\"invalid\"}")] + public void Read_ShouldThrowJsonException_OnInvalidOrMissingType(string json) + { + // Arrange + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act & Assert + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + [Theory] + [InlineData("{\"type\":\"card\"}")] + [InlineData("{\"type\":\"card\",\"token\":\"\"}")] + [InlineData("{\"type\":\"card\",\"token\":null}")] + public void Read_ShouldThrowJsonException_OnInvalidTokenizedPaymentMethodToken(string json) + { + // Arrange + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act & Assert + Assert.Throws(() => JsonSerializer.Deserialize(json, options)); + } + + // Tokenized payment method deserialization + [Theory] + [InlineData("bankAccount", TokenizablePaymentMethodType.BankAccount)] + [InlineData("card", TokenizablePaymentMethodType.Card)] + [InlineData("payPal", TokenizablePaymentMethodType.PayPal)] + public void Read_ShouldDeserializeTokenizedPaymentMethods(string typeString, TokenizablePaymentMethodType expectedType) + { + // Arrange + var json = $"{{\"type\":\"{typeString}\",\"token\":\"test-token\"}}"; + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.True(result.IsTokenized); + Assert.Equal(expectedType, result.AsT0.Type); + Assert.Equal("test-token", result.AsT0.Token); + } + + // Non-tokenized payment method deserialization + [Theory] + [InlineData("accountcredit", NonTokenizablePaymentMethodType.AccountCredit)] + public void Read_ShouldDeserializeNonTokenizedPaymentMethods(string typeString, NonTokenizablePaymentMethodType expectedType) + { + // Arrange + var json = $"{{\"type\":\"{typeString}\"}}"; + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act + var result = JsonSerializer.Deserialize(json, options); + + // Assert + Assert.True(result.IsNonTokenized); + Assert.Equal(expectedType, result.AsT1.Type); + } + + // Tokenized payment method serialization + [Theory] + [InlineData(TokenizablePaymentMethodType.BankAccount, "bankaccount")] + [InlineData(TokenizablePaymentMethodType.Card, "card")] + [InlineData(TokenizablePaymentMethodType.PayPal, "paypal")] + public void Write_ShouldSerializeTokenizedPaymentMethods(TokenizablePaymentMethodType type, string expectedTypeString) + { + // Arrange + var paymentMethod = new PaymentMethod(new TokenizedPaymentMethod + { + Type = type, + Token = "test-token" + }); + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act + var json = JsonSerializer.Serialize(paymentMethod, options); + + // Assert + Assert.Contains($"\"type\":\"{expectedTypeString}\"", json); + Assert.Contains("\"token\":\"test-token\"", json); + } + + // Non-tokenized payment method serialization + [Theory] + [InlineData(NonTokenizablePaymentMethodType.AccountCredit, "accountcredit")] + public void Write_ShouldSerializeNonTokenizedPaymentMethods(NonTokenizablePaymentMethodType type, string expectedTypeString) + { + // Arrange + var paymentMethod = new PaymentMethod(new NonTokenizedPaymentMethod { Type = type }); + var options = new JsonSerializerOptions { Converters = { new PaymentMethodJsonConverter() } }; + + // Act + var json = JsonSerializer.Serialize(paymentMethod, options); + + // Assert + Assert.Contains($"\"type\":\"{expectedTypeString}\"", json); + Assert.DoesNotContain("token", json); + } +} diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs index b6d497b7de..c0618f78ed 100644 --- a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Caches; +using Bit.Core.Billing; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; @@ -567,4 +568,79 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests var unhandled = result.AsT3; Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response); } + + [Theory, BitAutoData] + public async Task Run_AccountCredit_WithExistingCustomer_Success( + User user, + NonTokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = "existing_customer_123"; + paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "existing_customer_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "active"; + mockSubscription.Items = new StripeList + { + Data = + [ + new SubscriptionItem + { + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30) + } + ] + }; + + var mockInvoice = Substitute.For(); + + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + await _subscriberService.Received(1).GetCustomerOrThrow(Arg.Any(), Arg.Any()); + await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any()); + Assert.True(user.Premium); + Assert.Equal(mockSubscription.GetCurrentPeriodEnd(), user.PremiumExpirationDate); + } + + [Theory, BitAutoData] + public async Task Run_NonTokenizedPaymentWithoutExistingCustomer_ThrowsBillingException( + User user, + NonTokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + // No existing gateway customer ID + user.GatewayCustomerId = null; + paymentMethod.Type = NonTokenizablePaymentMethodType.AccountCredit; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + //Assert + Assert.True(result.IsT3); // Assuming T3 is the Unhandled result + Assert.IsType(result.AsT3.Exception); + // Verify no customer was created or subscription attempted + await _stripeAdapter.DidNotReceive().CustomerCreateAsync(Arg.Any()); + await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any()); + await _userService.DidNotReceive().SaveUserAsync(Arg.Any()); + } } From 0b4ce8765e7c35ea8a92252edc8a14dc4ac14598 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Fri, 24 Oct 2025 06:42:10 -0700 Subject: [PATCH 52/52] [PM-23904] Add risk insights for premium feature flag (#6491) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f7ce3aa59e..579fe0e253 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -241,6 +241,7 @@ public static class FeatureFlagKeys public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view"; public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp"; public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption"; + public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium"; /* Innovation Team */ public const string ArchiveVaultItems = "pm-19148-innovation-archive";