From 290fa3ded45227b6529565493a5eb5965fc3bfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 27 Jun 2025 15:29:34 +0100 Subject: [PATCH 001/326] [PM-22101] Enforce restrictions on collections with DefaultUserCollection type (#5968) * Add CreateCollectionCommand and associated interface with validation logic * Implement CreateCollectionCommand to handle collection creation with organization checks and access permissions. * Introduce ICreateCollectionCommand interface for defining the collection creation contract. * Add unit tests for CreateCollectionCommand to validate various scenarios including permission checks and error handling. * Add UpdateCollectionCommand and associated interface with validation logic * Implement UpdateCollectionCommand to handle collection updates with organization checks and access permissions. * Introduce IUpdateCollectionCommand interface for defining the collection update contract. * Add unit tests for UpdateCollectionCommand to validate various scenarios including permission checks and error handling. * Add scoped services for collection commands * Register ICreateCollectionCommand and IUpdateCollectionCommand in the service collection for handling collection creation and updates. * Refactor CollectionsController to use command interfaces for collection creation and updates * Updated CollectionsController to utilize ICreateCollectionCommand and IUpdateCollectionCommand for handling collection creation and updates, replacing calls to ICollectionService. * Adjusted related unit tests to verify the new command implementations. * Refactor ICollectionService and CollectionService to remove SaveAsync method * Removed the SaveAsync method from ICollectionService and its implementation in CollectionService. * Updated related tests in CollectionServiceTests to reflect the removal of SaveAsync, ensuring existing functionality remains intact. * Remove unused organization repository dependency from CollectionServiceTests * Add validation to CreateCollectionCommand to prevent creation of DefaultUserCollection type * Implemented a check in CreateCollectionCommand to throw a BadRequestException if a collection of type DefaultUserCollection is attempted to be created. * Added a unit test to verify that the exception is thrown with the correct message when attempting to create a collection of this type. * Add validation to DeleteCollectionCommand to prevent deletion of DefaultUserCollection type * Implemented checks in DeleteAsync and DeleteManyAsync methods to throw a BadRequestException if a collection of type DefaultUserCollection is attempted to be deleted. * Added unit tests to verify that the exceptions are thrown with the correct messages when attempting to delete collections of this type. * Add validation in UpdateCollectionCommand to prevent editing DefaultUserCollection type * Implemented a check in UpdateAsync to throw a BadRequestException if a collection of type DefaultUserCollection is attempted to be updated. * Added a unit test to verify that the exception is thrown with the correct message when attempting to update a collection of this type. * Add validation in UpdateOrganizationUserCommand to prevent modification of DefaultUserCollection type * Implemented a check to throw a BadRequestException if an attempt is made to modify member access for collections of type DefaultUserCollection. * Added a unit test to ensure the exception is thrown with the correct message when this condition is met. * Add validation in UpdateGroupCommand to prevent modification of DefaultUserCollection type * Implemented a check to throw a BadRequestException if an attempt is made to modify group access for collections of type DefaultUserCollection. * Added a unit test to ensure the exception is thrown with the correct message when this condition is met. * Add validation in BulkAddCollectionAccessCommand to prevent addition of collections of DefaultUserCollection type * Implemented a check to throw a BadRequestException if an attempt is made to add access to collections of type DefaultUserCollection. * Added a unit test to ensure the exception is thrown with the correct message when this condition is met. * Add validation in CollectionService to prevent modification of DefaultUserCollection type * Implemented a check in DeleteUserAsync to throw a BadRequestException if an attempt is made to modify member access for collections of type DefaultUserCollection. * Added a unit test to ensure the exception is thrown with the correct message when this condition is met. * Implement a check to throw a BadRequestException if an attempt is made to modify member access for collections of type DefaultUserCollection. * Add validation in CollectionsController to prevent deletion of DefaultUserCollection type * Implemented a check to return a BadRequestObjectResult if an attempt is made to delete a collection of type DefaultUserCollection. * Remove unused test method for handling DefaultUserCollection in CollectionsControllerTests * Update UpdateOrganizationUserCommandTests to use OrganizationUserType for user updates --- .../Controllers/CollectionsController.cs | 7 +++ .../Groups/UpdateGroupCommand.cs | 5 +++ .../UpdateOrganizationUserCommand.cs | 5 +++ .../BulkAddCollectionAccessCommand.cs | 5 +++ .../CreateCollectionCommand.cs | 6 +++ .../DeleteCollectionCommand.cs | 12 ++++++ .../UpdateCollectionCommand.cs | 6 +++ .../Implementations/CollectionService.cs | 7 ++- .../Groups/UpdateGroupCommandTests.cs | 18 ++++++++ .../UpdateOrganizationUserCommandTests.cs | 18 ++++++++ .../BulkAddCollectionAccessCommandTests.cs | 43 +++++++++++++++++++ .../CreateCollectionCommandTests.cs | 23 ++++++++++ .../DeleteCollectionCommandTests.cs | 42 +++++++++++++++++- .../UpdateCollectionCommandTests.cs | 22 ++++++++++ .../Services/CollectionServiceTests.cs | 18 ++++++++ 15 files changed, 233 insertions(+), 4 deletions(-) diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index d4e0932caa..ec282a0e4d 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -2,6 +2,7 @@ using Bit.Api.Models.Public.Request; using Bit.Api.Models.Public.Response; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -116,6 +117,12 @@ public class CollectionsController : Controller { return new NotFoundResult(); } + + if (collection.Type == CollectionType.DefaultUserCollection) + { + return new BadRequestObjectResult(new ErrorResponseModel("You cannot delete a collection with the type as DefaultUserCollection.")); + } + await _collectionRepository.DeleteAsync(collection); return new OkResult(); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs index 1b53716537..3347e77c37 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommand.cs @@ -163,6 +163,11 @@ public class UpdateGroupCommand : IUpdateGroupCommand // Use generic error message to avoid enumeration throw new NotFoundException(); } + + if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection)) + { + throw new BadRequestException("You cannot modify group access for collections with the type as DefaultUserCollection."); + } } private async Task ValidateMemberAccessAsync(Group originalGroup, diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index 8d1e693e8b..b72505c195 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -199,6 +199,11 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand // Use generic error message to avoid enumeration throw new NotFoundException(); } + + if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection)) + { + throw new BadRequestException("You cannot modify member access for collections with the type as DefaultUserCollection."); + } } private async Task ValidateGroupAccessAsync(OrganizationUser originalUser, diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommand.cs index 1d7eb8f2ba..929c236ef2 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommand.cs @@ -52,6 +52,11 @@ public class BulkAddCollectionAccessCommand : IBulkAddCollectionAccessCommand throw new BadRequestException("No collections were provided."); } + if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection)) + { + throw new BadRequestException("You cannot add access to collections with the type as DefaultUserCollection."); + } + var orgId = collections.First().OrganizationId; if (collections.Any(c => c.OrganizationId != orgId)) diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs index 1cec2f5cc4..d83e30ad9c 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; @@ -26,6 +27,11 @@ public class CreateCollectionCommand : ICreateCollectionCommand public async Task CreateAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null) { + if (collection.Type == CollectionType.DefaultUserCollection) + { + throw new BadRequestException("You cannot create a collection with the type as DefaultUserCollection."); + } + var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); if (org == null) { diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommand.cs index 11f29f228f..4f678633a9 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommand.cs @@ -1,4 +1,6 @@ using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -20,6 +22,11 @@ public class DeleteCollectionCommand : IDeleteCollectionCommand public async Task DeleteAsync(Collection collection) { + if (collection.Type == CollectionType.DefaultUserCollection) + { + throw new BadRequestException("You cannot delete a collection with the type as DefaultUserCollection."); + } + await _collectionRepository.DeleteAsync(collection); await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Deleted, DateTime.UtcNow); } @@ -33,6 +40,11 @@ public class DeleteCollectionCommand : IDeleteCollectionCommand public async Task DeleteManyAsync(IEnumerable collections) { + if (collections.Any(c => c.Type == Enums.CollectionType.DefaultUserCollection)) + { + throw new BadRequestException("You cannot delete collections with the type as DefaultUserCollection."); + } + await _collectionRepository.DeleteManyAsync(collections.Select(c => c.Id)); await _eventService.LogCollectionEventsAsync(collections.Select(c => (c, Enums.EventType.Collection_Deleted, (DateTime?)DateTime.UtcNow))); } diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs index 3985b6a919..19ad47a0a5 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; @@ -26,6 +27,11 @@ public class UpdateCollectionCommand : IUpdateCollectionCommand public async Task UpdateAsync(Collection collection, IEnumerable groups = null, IEnumerable users = null) { + if (collection.Type == CollectionType.DefaultUserCollection) + { + throw new BadRequestException("You cannot edit a collection with the type as DefaultUserCollection."); + } + var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId); if (org == null) { diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 2a3f8c42dc..3b828955af 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -22,10 +22,13 @@ public class CollectionService : ICollectionService _collectionRepository = collectionRepository; } - - public async Task DeleteUserAsync(Collection collection, Guid organizationUserId) { + if (collection.Type == Enums.CollectionType.DefaultUserCollection) + { + throw new BadRequestException("You cannot modify member access for collections with the type as DefaultUserCollection."); + } + var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); if (orgUser == null || orgUser.OrganizationId != collection.OrganizationId) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommandTests.cs index 41486e1c00..b9f6964123 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Groups/UpdateGroupCommandTests.cs @@ -156,6 +156,24 @@ public class UpdateGroupCommandTests () => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess)); } + [Theory, OrganizationCustomize(UseGroups = true), BitAutoData] + public async Task UpdateGroup_WithDefaultUserCollectionType_Throws(SutProvider sutProvider, + Group group, Group oldGroup, Organization organization, List collectionAccess) + { + ArrangeGroup(sutProvider, group, oldGroup); + ArrangeUsers(sutProvider, group); + + // Return collections with DefaultUserCollection type + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(callInfo => callInfo.Arg>() + .Select(guid => new Collection { Id = guid, OrganizationId = group.OrganizationId, Type = CollectionType.DefaultUserCollection }).ToList()); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateGroupAsync(group, organization, collectionAccess)); + Assert.Contains("You cannot modify group access for collections with the type as DefaultUserCollection.", exception.Message); + } + [Theory, OrganizationCustomize(UseGroups = true), BitAutoData] public async Task UpdateGroup_MemberBelongsToDifferentOrganization_Throws(SutProvider sutProvider, Group group, Group oldGroup, Organization organization, IEnumerable userAccess) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index 5c07ce0347..bd112c5d40 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -244,6 +244,24 @@ public class UpdateOrganizationUserCommandTests Assert.Contains("User can only be an admin of one free organization.", exception.Message); } + [Theory, BitAutoData] + public async Task UpdateUserAsync_WithDefaultUserCollectionType_Throws(OrganizationUser user, OrganizationUser originalUser, + List collectionAccess, Guid? savingUserId, SutProvider sutProvider, + Organization organization) + { + Setup(sutProvider, organization, user, originalUser); + + // Return collections with DefaultUserCollection type + sutProvider.GetDependency() + .GetManyByManyIdsAsync(Arg.Any>()) + .Returns(callInfo => callInfo.Arg>() + .Select(guid => new Collection { Id = guid, OrganizationId = user.OrganizationId, Type = CollectionType.DefaultUserCollection }).ToList()); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.UpdateUserAsync(user, OrganizationUserType.User, savingUserId, collectionAccess, null)); + Assert.Contains("You cannot modify member access for collections with the type as DefaultUserCollection.", exception.Message); + } + private void Setup(SutProvider sutProvider, Organization organization, OrganizationUser newUser, OrganizationUser oldUser) { diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommandTests.cs index 63f2bac896..713edeefbf 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/BulkAddCollectionAccessCommandTests.cs @@ -27,6 +27,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + sutProvider.GetDependency() .GetManyAsync( Arg.Is>(ids => ids.SequenceEqual(collectionUsers.Select(u => u.OrganizationUserId))) @@ -107,6 +109,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + collections.First().OrganizationId = Guid.NewGuid(); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.AddAccessAsync(collections, @@ -127,6 +131,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + organizationUsers.RemoveAt(0); sutProvider.GetDependency() @@ -155,6 +161,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + organizationUsers.First().OrganizationId = Guid.NewGuid(); sutProvider.GetDependency() @@ -184,6 +192,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + groups.RemoveAt(0); sutProvider.GetDependency() @@ -221,6 +231,8 @@ public class BulkAddCollectionAccessCommandTests IEnumerable collectionUsers, IEnumerable collectionGroups) { + SetCollectionsToSharedType(collections); + groups.First().OrganizationId = Guid.NewGuid(); sutProvider.GetDependency() @@ -250,6 +262,37 @@ public class BulkAddCollectionAccessCommandTests ); } + [Theory, BitAutoData, CollectionCustomization] + public async Task AddAccessAsync_WithDefaultUserCollectionType_ThrowsBadRequest(SutProvider sutProvider, + IList collections, + IEnumerable collectionUsers, + IEnumerable collectionGroups) + { + // Arrange + collections.First().Type = CollectionType.DefaultUserCollection; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.AddAccessAsync(collections, + ToAccessSelection(collectionUsers), + ToAccessSelection(collectionGroups) + )); + + Assert.Contains("You cannot add access to collections with the type as DefaultUserCollection.", exception.Message); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateOrUpdateAccessForManyAsync(default, default, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().LogCollectionEventsAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByManyIds(default); + } + + private static void SetCollectionsToSharedType(IEnumerable collections) + { + foreach (var collection in collections) + { + collection.Type = CollectionType.SharedCollection; + } + } + private static ICollection ToAccessSelection(IEnumerable collectionUsers) { return collectionUsers.Select(cu => new CollectionAccessSelection diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs index d180bad432..8937e8628d 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateCollectionCommandTests.cs @@ -199,4 +199,27 @@ public class CreateCollectionCommandTests .DidNotReceiveWithAnyArgs() .LogCollectionEventAsync(default, default); } + + [Theory, BitAutoData] + public async Task CreateAsync_WithDefaultUserCollectionType_ThrowsBadRequest( + Organization organization, Collection collection, SutProvider sutProvider) + { + collection.Id = default; + collection.Type = CollectionType.DefaultUserCollection; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(collection)); + Assert.Contains("You cannot create a collection with the type as DefaultUserCollection.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommandTests.cs index 99eca20a09..efe9223f1b 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteCollectionCommandTests.cs @@ -1,6 +1,6 @@ - -using Bit.Core.Entities; +using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationCollections; using Bit.Core.Repositories; using Bit.Core.Services; @@ -34,6 +34,7 @@ public class DeleteCollectionCommandTests { // Arrange var collectionIds = new[] { collection.Id, collection2.Id }; + collection.Type = collection2.Type = CollectionType.SharedCollection; sutProvider.GetDependency() .GetManyByManyIdsAsync(collectionIds) @@ -51,5 +52,42 @@ public class DeleteCollectionCommandTests a.All(c => collectionIds.Contains(c.Item1.Id) && c.Item2 == EventType.Collection_Deleted))); } + [Theory, BitAutoData] + [OrganizationCustomize] + public async Task DeleteAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAsync(collection)); + Assert.Contains("You cannot delete a collection with the type as DefaultUserCollection.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default, default); + } + + [Theory, BitAutoData] + [OrganizationCustomize] + public async Task DeleteManyAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, Collection collection2, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + collection2.Type = CollectionType.SharedCollection; + var collections = new List { collection, collection2 }; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAsync(collections)); + Assert.Contains("You cannot delete collections with the type as DefaultUserCollection.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventsAsync(default); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs index 5147157750..2b8c180989 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommandTests.cs @@ -166,4 +166,26 @@ public class UpdateCollectionCommandTests .DidNotReceiveWithAnyArgs() .LogCollectionEventAsync(default, default); } + + [Theory, BitAutoData] + public async Task UpdateAsync_WithDefaultUserCollectionType_ThrowsBadRequest( + Organization organization, Collection collection, SutProvider sutProvider) + { + collection.Type = CollectionType.DefaultUserCollection; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(collection)); + Assert.Contains("You cannot edit a collection with the type as DefaultUserCollection.", ex.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogCollectionEventAsync(default, default); + } } diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs index 2f99467700..118c0fa6b2 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -49,4 +49,22 @@ public class CollectionServiceTest await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .LogOrganizationUserEventAsync(default, default); } + + [Theory, BitAutoData] + public async Task DeleteUserAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, + Organization organization, OrganizationUser organizationUser, SutProvider sutProvider) + { + collection.Type = CollectionType.DefaultUserCollection; + collection.OrganizationId = organization.Id; + organizationUser.OrganizationId = organization.Id; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.DeleteUserAsync(collection, organizationUser.Id)); + Assert.Contains("You cannot modify member access for collections with the type as DefaultUserCollection.", exception.Message); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync(default, default); + } } From 8bccf255c08b68bb97568a81d3f6a6bb84112439 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 27 Jun 2025 11:17:47 -0500 Subject: [PATCH 002/326] [PM-22974] Cascade delete NotificationStatus entities (#6011) * cascade delete NotificationStatus entities * add userId to test for foreign constraint * add missing properties for Notification * add check for foreign key --- ...tificationStatusEntityTypeConfiguration.cs | 6 ++++ .../Notification_MarkAsDeletedByTask.sql | 0 src/Sql/dbo/Tables/NotificationStatus.sql | 3 +- .../Repositories/CipherRepositoryTests.cs | 29 ++++++++++++++++++- ...-26_01_CascadeDeleteNotificationStatus.sql | 10 +++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql create mode 100644 util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs index 1207d839d8..c535cd3e84 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs @@ -13,6 +13,12 @@ public class NotificationStatusEntityTypeConfiguration : IEntityTypeConfiguratio .HasKey(ns => new { ns.UserId, ns.NotificationId }) .IsClustered(); + builder + .HasOne(ns => ns.Notification) + .WithMany() + .HasForeignKey(ns => ns.NotificationId) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(nameof(NotificationStatus)); } } diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Sql/dbo/Tables/NotificationStatus.sql b/src/Sql/dbo/Tables/NotificationStatus.sql index 2f68e2b2f7..2ccb0e0a8a 100644 --- a/src/Sql/dbo/Tables/NotificationStatus.sql +++ b/src/Sql/dbo/Tables/NotificationStatus.sql @@ -5,11 +5,10 @@ CREATE TABLE [dbo].[NotificationStatus] [ReadDate] DATETIME2 (7) NULL, [DeletedDate] DATETIME2 (7) NULL, CONSTRAINT [PK_NotificationStatus] PRIMARY KEY CLUSTERED ([NotificationId] ASC, [UserId] ASC), - CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification] ([Id]), + CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_NotificationStatus_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); GO CREATE NONCLUSTERED INDEX [IX_NotificationStatus_UserId] ON [dbo].[NotificationStatus]([UserId] ASC); - diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 288752011f..0a186e43be 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -5,6 +5,8 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; @@ -976,8 +978,11 @@ public class CipherRepositoryTests [DatabaseTheory, DatabaseData] public async Task DeleteCipherWithSecurityTaskAsync_Works( IOrganizationRepository organizationRepository, + IUserRepository userRepository, ICipherRepository cipherRepository, - ISecurityTaskRepository securityTaskRepository) + ISecurityTaskRepository securityTaskRepository, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) { var organization = await organizationRepository.CreateAsync(new Organization { @@ -987,6 +992,14 @@ public class CipherRepositoryTests BillingEmail = "" }); + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; await cipherRepository.CreateAsync(cipher1); @@ -1012,6 +1025,20 @@ public class CipherRepositoryTests }; await securityTaskRepository.CreateManyAsync(tasks); + var notification = await notificationRepository.CreateAsync(new Notification + { + OrganizationId = organization.Id, + UserId = user.Id, + TaskId = tasks[1].Id, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + }); + await notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notification.Id, + UserId = user.Id, + ReadDate = DateTime.UtcNow, + }); // Delete cipher with pending security task await cipherRepository.DeleteAsync(cipher1); diff --git a/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql b/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql new file mode 100644 index 0000000000..474ac14a7a --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql @@ -0,0 +1,10 @@ +BEGIN + IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_NotificationStatus_Notification') + BEGIN + ALTER TABLE [dbo].[NotificationStatus] DROP CONSTRAINT [FK_NotificationStatus_Notification] + END + + ALTER TABLE [dbo].[NotificationStatus] + ADD CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification]([Id]) ON DELETE CASCADE +END +GO From c441fa27dd3e5eb787c4eb5d293136e52f6bca52 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 27 Jun 2025 14:13:41 -0400 Subject: [PATCH 003/326] Removing feature flag (#5997) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 066d73f6d1..b2e28dab47 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,7 +109,6 @@ public static class FeatureFlagKeys public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; - public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript"; public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; From 69b7600eabde42b9cf3acb943ecff4e832404162 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 27 Jun 2025 16:04:47 -0500 Subject: [PATCH 004/326] [PM-20041] Deleting Notifications when Task is completed (#5896) * mark all notifications associated with a security task as deleted when the task is completed * fix spelling * formatting * refactor "Active" to "NonDeleted" * refactor "Active" to "NonDeleted" for stored procedure * only send notifications per user for each notification * move notification status updates into the DB layer to save on multiple queries and insertions from the C# * Only return UserIds from db layer * omit userId from `MarkTaskAsCompletedCommand` query. The userId from the notification will be used * update UserIds * consistency in comments regarding `taskId` and `UserId` --- .../Repositories/INotificationRepository.cs | 9 ++++ ...arkNotificationsForTaskAsDeletedCommand.cs | 11 +++++ ...arkNotificationsForTaskAsDeletedCommand.cs | 32 ++++++++++++ .../Commands/MarkTaskAsCompletedCommand.cs | 9 +++- .../Vault/Repositories/ICipherRepository.cs | 2 +- .../Vault/VaultServiceCollectionExtensions.cs | 1 + .../Repositories/NotificationRepository.cs | 17 +++++++ .../Repositories/NotificationRepository.cs | 49 +++++++++++++++++++ .../Notification_MarkAsDeletedByTask.sql | 35 +++++++++++++ ...30_00_Notification_MarkAsDeletedByTask.sql | 35 +++++++++++++ 10 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs create mode 100644 src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs create mode 100644 util/Migrator/DbScripts/2025-05-30_00_Notification_MarkAsDeletedByTask.sql diff --git a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs index 21604ed169..0d0ea80491 100644 --- a/src/Core/NotificationCenter/Repositories/INotificationRepository.cs +++ b/src/Core/NotificationCenter/Repositories/INotificationRepository.cs @@ -32,4 +32,13 @@ public interface INotificationRepository : IRepository /// Task> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions); + + /// + /// Marks notifications as deleted by a taskId. + /// + /// The unique identifier of the task. + /// + /// A collection of UserIds for the notifications that are now marked as deleted. + /// + Task> MarkNotificationsAsDeletedByTask(Guid taskId); } diff --git a/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs new file mode 100644 index 0000000000..90566b4d83 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/IMarkNotificationsForTaskAsDeletedCommand.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface IMarkNotificationsForTaskAsDeletedCommand +{ + /// + /// Marks notifications associated with a given taskId as deleted. + /// + /// The unique identifier of the task to complete + /// A task representing the async operation + Task MarkAsDeletedAsync(Guid taskId); +} diff --git a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs new file mode 100644 index 0000000000..8d1e6e4538 --- /dev/null +++ b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs @@ -0,0 +1,32 @@ +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Platform.Push; +using Bit.Core.Vault.Commands.Interfaces; + +namespace Bit.Core.Vault.Commands; + +public class MarkNotificationsForTaskAsDeletedCommand : IMarkNotificationsForTaskAsDeletedCommand +{ + private readonly INotificationRepository _notificationRepository; + private readonly IPushNotificationService _pushNotificationService; + + public MarkNotificationsForTaskAsDeletedCommand( + INotificationRepository notificationRepository, + IPushNotificationService pushNotificationService) + { + _notificationRepository = notificationRepository; + _pushNotificationService = pushNotificationService; + + } + + public async Task MarkAsDeletedAsync(Guid taskId) + { + var userIds = await _notificationRepository.MarkNotificationsAsDeletedByTask(taskId); + + // For each user associated with the notifications, send a push notification so local tasks can be updated. + var uniqueUserIds = userIds.Distinct(); + foreach (var id in uniqueUserIds) + { + await _pushNotificationService.PushPendingSecurityTasksAsync(id); + } + } +} diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs index 77b8a8625c..8a12910bb8 100644 --- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -14,15 +14,19 @@ public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand private readonly ISecurityTaskRepository _securityTaskRepository; private readonly IAuthorizationService _authorizationService; private readonly ICurrentContext _currentContext; + private readonly IMarkNotificationsForTaskAsDeletedCommand _markNotificationsForTaskAsDeletedAsync; + public MarkTaskAsCompletedCommand( ISecurityTaskRepository securityTaskRepository, IAuthorizationService authorizationService, - ICurrentContext currentContext) + ICurrentContext currentContext, + IMarkNotificationsForTaskAsDeletedCommand markNotificationsForTaskAsDeletedAsync) { _securityTaskRepository = securityTaskRepository; _authorizationService = authorizationService; _currentContext = currentContext; + _markNotificationsForTaskAsDeletedAsync = markNotificationsForTaskAsDeletedAsync; } /// @@ -46,5 +50,8 @@ public class MarkTaskAsCompletedCommand : IMarkTaskAsCompleteCommand task.RevisionDate = DateTime.UtcNow; await _securityTaskRepository.ReplaceAsync(task); + + // Mark all notifications related to this task as deleted + await _markNotificationsForTaskAsDeletedAsync.MarkAsDeletedAsync(taskId); } } diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 46742c6aa3..5a04a6651d 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -55,7 +55,7 @@ public interface ICipherRepository : IRepository Guid userId); /// - /// Returns the users and the cipher ids for security tawsks that are applicable to them. + /// Returns the users and the cipher ids for security tasks that are applicable to them. /// /// Security tasks are actionable when a user has manage access to the associated cipher. /// diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 1f361cb613..9efa1ea379 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -24,5 +24,6 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs index b6843d9801..63b1c21f49 100644 --- a/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.Dapper/NotificationCenter/Repositories/NotificationRepository.cs @@ -56,4 +56,21 @@ public class NotificationRepository : Repository, INotificat ContinuationToken = data.Count < pageOptions.PageSize ? null : (pageNumber + 1).ToString() }; } + + public async Task> MarkNotificationsAsDeletedByTask(Guid taskId) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + "[dbo].[Notification_MarkAsDeletedByTask]", + new + { + TaskId = taskId, + }, + commandType: CommandType.StoredProcedure); + + var data = results.ToList(); + + return data; + } } diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs index 5d1071f26c..213a14a81d 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Repositories/NotificationRepository.cs @@ -74,4 +74,53 @@ public class NotificationRepository : Repository> MarkNotificationsAsDeletedByTask(Guid taskId) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var notifications = await dbContext.Notifications + .Where(n => n.TaskId == taskId) + .ToListAsync(); + + var notificationIds = notifications.Select(n => n.Id).ToList(); + + var statuses = await dbContext.Set() + .Where(ns => notificationIds.Contains(ns.NotificationId)) + .ToListAsync(); + + var now = DateTime.UtcNow; + + // Update existing statuses and add missing ones + foreach (var notification in notifications) + { + var status = statuses.FirstOrDefault(s => s.NotificationId == notification.Id); + if (status != null) + { + if (status.DeletedDate == null) + { + status.DeletedDate = now; + } + } + else if (notification.UserId.HasValue) + { + dbContext.Set().Add(new NotificationStatus + { + NotificationId = notification.Id, + UserId = (Guid)notification.UserId, + DeletedDate = now + }); + } + } + + await dbContext.SaveChangesAsync(); + + var userIds = notifications + .Select(n => n.UserId) + .Where(u => u.HasValue) + .ToList(); + + return (IEnumerable)userIds; + } } diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql index e69de29bb2..a2c16079f7 100644 --- a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql +++ b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql @@ -0,0 +1,35 @@ +CREATE PROCEDURE [dbo].[Notification_MarkAsDeletedByTask] + @TaskId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + -- Collect UserIds as they are altered + DECLARE @UserIdsForAlteredNotifications TABLE ( + UserId UNIQUEIDENTIFIER + ); + + -- Update existing NotificationStatus as deleted + UPDATE ns + SET ns.DeletedDate = GETUTCDATE() + OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications + FROM NotificationStatus ns + INNER JOIN Notification n ON ns.NotificationId = n.Id + WHERE n.TaskId = @TaskId + AND ns.DeletedDate IS NULL; + + -- Insert NotificationStatus records for notifications that don't have one yet + INSERT INTO NotificationStatus (NotificationId, UserId, DeletedDate) + OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications + SELECT n.Id, n.UserId, GETUTCDATE() + FROM Notification n + LEFT JOIN NotificationStatus ns + ON n.Id = ns.NotificationId + WHERE n.TaskId = @TaskId + AND ns.NotificationId IS NULL; + + -- Return the UserIds associated with the altered notifications + SELECT u.UserId + FROM @UserIdsForAlteredNotifications u; +END +GO diff --git a/util/Migrator/DbScripts/2025-05-30_00_Notification_MarkAsDeletedByTask.sql b/util/Migrator/DbScripts/2025-05-30_00_Notification_MarkAsDeletedByTask.sql new file mode 100644 index 0000000000..43989a4cac --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-30_00_Notification_MarkAsDeletedByTask.sql @@ -0,0 +1,35 @@ +CREATE OR ALTER PROCEDURE [dbo].[Notification_MarkAsDeletedByTask] + @TaskId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + -- Collect UserIds as they are altered + DECLARE @UserIdsForAlteredNotifications TABLE ( + UserId UNIQUEIDENTIFIER + ); + + -- Update existing NotificationStatus as deleted + UPDATE ns + SET ns.DeletedDate = GETUTCDATE() + OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications + FROM NotificationStatus ns + INNER JOIN Notification n ON ns.NotificationId = n.Id + WHERE n.TaskId = @TaskId + AND ns.DeletedDate IS NULL; + + -- Insert NotificationStatus records for notifications that don't have one yet + INSERT INTO NotificationStatus (NotificationId, UserId, DeletedDate) + OUTPUT inserted.UserId INTO @UserIdsForAlteredNotifications + SELECT n.Id, n.UserId, GETUTCDATE() + FROM Notification n + LEFT JOIN NotificationStatus ns + ON n.Id = ns.NotificationId + WHERE n.TaskId = @TaskId + AND ns.NotificationId IS NULL; + + -- Return the UserIds associated with the altered notifications + SELECT u.UserId + FROM @UserIdsForAlteredNotifications u; +END +GO From 386b391dd7f1560cbf47cf843baac1fcb1c52f44 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 30 Jun 2025 08:08:01 -0400 Subject: [PATCH 005/326] Remove `GetManyDetailsByUserAsync` & `ReplaceAsync` DB Calls (#6012) --- src/Api/Controllers/DevicesController.cs | 6 +++++- src/Core/Services/IDeviceService.cs | 2 +- src/Core/Services/Implementations/DeviceService.cs | 12 ++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index 0ff4e93abe..eaa572b7ec 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -206,7 +206,11 @@ public class DevicesController : Controller throw new NotFoundException(); } - await _deviceService.SaveAsync(model.ToData(), device); + await _deviceService.SaveAsync( + model.ToData(), + device, + _currentContext.Organizations.Select(org => org.Id.ToString()) + ); } [AllowAnonymous] diff --git a/src/Core/Services/IDeviceService.cs b/src/Core/Services/IDeviceService.cs index cd055f8b46..78739e081d 100644 --- a/src/Core/Services/IDeviceService.cs +++ b/src/Core/Services/IDeviceService.cs @@ -6,7 +6,7 @@ namespace Bit.Core.Services; public interface IDeviceService { - Task SaveAsync(WebPushRegistrationData webPush, Device device); + Task SaveAsync(WebPushRegistrationData webPush, Device device, IEnumerable organizationIds); Task SaveAsync(Device device); Task ClearTokenAsync(Device device); Task DeactivateAsync(Device device); diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 165fab0237..931dfccdec 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -29,9 +29,17 @@ public class DeviceService : IDeviceService _globalSettings = globalSettings; } - public async Task SaveAsync(WebPushRegistrationData webPush, Device device) + public async Task SaveAsync(WebPushRegistrationData webPush, Device device, IEnumerable organizationIds) { - await SaveAsync(new PushRegistrationData(webPush.Endpoint, webPush.P256dh, webPush.Auth), device); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync( + new PushRegistrationData(webPush.Endpoint, webPush.P256dh, webPush.Auth), + device.Id.ToString(), + device.UserId.ToString(), + device.Identifier, + device.Type, + organizationIds, + _globalSettings.Installation.Id + ); } public async Task SaveAsync(Device device) From 4eb2134e101694a19f87a891c456299078734d7d Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 30 Jun 2025 13:12:58 +0000 Subject: [PATCH 006/326] Bumped version to 2025.7.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6a1a305e84..e68ae70fb1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.6.2 + 2025.7.0 Bit.$(MSBuildProjectName) enable From 1da39aa2b89bc20a247d012bb143dbc3fa508d75 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 30 Jun 2025 09:45:15 -0400 Subject: [PATCH 007/326] [PM-22405] Add debugging instrument for finding invalid OrganizationUser state. (#5955) --- .../Implementations/OrganizationService.cs | 5 + .../UserInviteDebuggingLogger.cs | 67 +++++++++++ .../OrganizationUserRepository.cs | 14 ++- .../UserInviteDebuggingLoggerTests.cs | 113 ++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 src/Core/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLogger.cs create mode 100644 test/Core.Test/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLoggerTests.cs diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 7947cbefff..d648eef2c9 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Constants; @@ -900,6 +901,8 @@ public class OrganizationService : IOrganizationService IEnumerable organizationUsersId) { var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId); + _logger.LogUserInviteStateDiagnostics(orgUsers); + var org = await GetOrgById(organizationId); var result = new List>(); @@ -928,6 +931,8 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("User invalid."); } + _logger.LogUserInviteStateDiagnostics(orgUser); + var org = await GetOrgById(orgUser.OrganizationId); await SendInviteAsync(orgUser, org, initOrganization); } diff --git a/src/Core/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLogger.cs b/src/Core/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLogger.cs new file mode 100644 index 0000000000..6a0e581522 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLogger.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Microsoft.Extensions.Logging; +using Quartz.Util; + +namespace Bit.Core.AdminConsole.Utilities.DebuggingInstruments; + +/// +/// Temporary code: Log warning when OrganizationUser is in an invalid state, +/// so we can identify which flow is causing the issue through Datadog. +/// +public static class UserInviteDebuggingLogger +{ + public static void LogUserInviteStateDiagnostics(this ILogger logger, OrganizationUser orgUser) + { + LogUserInviteStateDiagnostics(logger, [orgUser]); + } + + public static void LogUserInviteStateDiagnostics(this ILogger logger, IEnumerable allOrgUsers) + { + try + { + var invalidInviteState = allOrgUsers.Any(user => user.Status == OrganizationUserStatusType.Invited && user.Email.IsNullOrWhiteSpace()); + + if (invalidInviteState) + { + var logData = MapObjectDataToLog(allOrgUsers); + logger.LogWarning("Warning invalid invited state. {logData}", logData); + } + + var invalidConfirmedOrAcceptedState = allOrgUsers.Any(user => (user.Status == OrganizationUserStatusType.Confirmed || user.Status == OrganizationUserStatusType.Accepted) && !user.Email.IsNullOrWhiteSpace()); + + if (invalidConfirmedOrAcceptedState) + { + var logData = MapObjectDataToLog(allOrgUsers); + logger.LogWarning("Warning invalid confirmed or accepted state. {logData}", logData); + } + } + catch (Exception exception) + { + + // Ensure that this debugging instrument does not interfere with the current flow. + logger.LogWarning(exception, "Unexpected exception from UserInviteDebuggingLogger"); + } + } + + private static string MapObjectDataToLog(IEnumerable allOrgUsers) + { + var log = allOrgUsers.Select(allOrgUser => new + { + allOrgUser.OrganizationId, + allOrgUser.Status, + hasEmail = !allOrgUser.Email.IsNullOrWhiteSpace(), + userId = allOrgUser.UserId, + allOrgUserId = allOrgUser.Id + }); + + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + return JsonSerializer.Serialize(log, options); + } +} diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 5a6fcbe4aa..feecf4a5d1 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.KeyManagement.UserKey; @@ -12,6 +13,7 @@ using Bit.Core.Repositories; using Bit.Core.Settings; using Dapper; using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; #nullable enable @@ -25,8 +27,9 @@ public class OrganizationUserRepository : Repository, IO /// https://github.com/dotnet/SqlClient/issues/54 /// private string _marsConnectionString; + private readonly ILogger _logger; - public OrganizationUserRepository(GlobalSettings globalSettings) + public OrganizationUserRepository(GlobalSettings globalSettings, ILogger logger) : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) { var builder = new SqlConnectionStringBuilder(ConnectionString) @@ -34,6 +37,7 @@ public class OrganizationUserRepository : Repository, IO MultipleActiveResultSets = true, }; _marsConnectionString = builder.ToString(); + _logger = logger; } public async Task GetCountByOrganizationIdAsync(Guid organizationId) @@ -305,6 +309,8 @@ public class OrganizationUserRepository : Repository, IO public async Task CreateAsync(OrganizationUser obj, IEnumerable collections) { + _logger.LogUserInviteStateDiagnostics(obj); + obj.SetNewId(); var objWithCollections = JsonSerializer.Deserialize( JsonSerializer.Serialize(obj))!; @@ -323,6 +329,8 @@ public class OrganizationUserRepository : Repository, IO public async Task ReplaceAsync(OrganizationUser obj, IEnumerable collections) { + _logger.LogUserInviteStateDiagnostics(obj); + var objWithCollections = JsonSerializer.Deserialize( JsonSerializer.Serialize(obj))!; objWithCollections.Collections = collections.ToArrayTVP(); @@ -406,6 +414,8 @@ public class OrganizationUserRepository : Repository, IO public async Task?> CreateManyAsync(IEnumerable organizationUsers) { + _logger.LogUserInviteStateDiagnostics(organizationUsers); + organizationUsers = organizationUsers.ToList(); if (!organizationUsers.Any()) { @@ -430,6 +440,8 @@ public class OrganizationUserRepository : Repository, IO public async Task ReplaceManyAsync(IEnumerable organizationUsers) { + _logger.LogUserInviteStateDiagnostics(organizationUsers); + organizationUsers = organizationUsers.ToList(); if (!organizationUsers.Any()) { diff --git a/test/Core.Test/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLoggerTests.cs b/test/Core.Test/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLoggerTests.cs new file mode 100644 index 0000000000..a8f14bf1dc --- /dev/null +++ b/test/Core.Test/AdminConsole/Utilities/DebuggingInstruments/UserInviteDebuggingLoggerTests.cs @@ -0,0 +1,113 @@ +using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Utilities.DebuggingInstruments; + +public class UserInviteDebuggingLoggerTests +{ + [Fact] + public void LogUserInviteStateDiagnostics_WhenInvitedUserHasNoEmail_LogsWarning() + { + // Arrange + var organizationUser = new OrganizationUser + { + OrganizationId = Guid.Parse("3e1f2196-9ad6-4ba7-b69d-ba33bc25f774"), + Status = OrganizationUserStatusType.Invited, + Email = string.Empty, + UserId = Guid.Parse("93fbddd1-e96d-491d-a38b-6966ff59ac28"), + Id = Guid.Parse("326f043f-afdc-47e5-9646-a76ab709b69a"), + }; + + var logger = Substitute.For(); + + // Act + logger.LogUserInviteStateDiagnostics(organizationUser); + + // Assert + logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(errorMessage => + errorMessage.ToString().Contains("Warning invalid invited state") + && errorMessage.ToString().Contains(organizationUser.OrganizationId.ToString()) + && errorMessage.ToString().Contains(organizationUser.UserId.ToString()) + && errorMessage.ToString().Contains(organizationUser.Id.ToString()) + ), + null, + Arg.Any>() + ); + } + + public static IEnumerable ConfirmedOrAcceptedTestCases => + [ + new object[] { OrganizationUserStatusType.Accepted }, + new object[] { OrganizationUserStatusType.Confirmed }, + ]; + + [Theory] + [MemberData(nameof(ConfirmedOrAcceptedTestCases))] + public void LogUserInviteStateDiagnostics_WhenNonInvitedUserHasEmail_LogsWarning(OrganizationUserStatusType userStatusType) + { + // Arrange + var organizationUser = new OrganizationUser + { + OrganizationId = Guid.Parse("3e1f2196-9ad6-4ba7-b69d-ba33bc25f774"), + Status = userStatusType, + Email = "someone@example.com", + UserId = Guid.Parse("93fbddd1-e96d-491d-a38b-6966ff59ac28"), + Id = Guid.Parse("326f043f-afdc-47e5-9646-a76ab709b69a"), + }; + + var logger = Substitute.For(); + + // Act + logger.LogUserInviteStateDiagnostics(organizationUser); + + // Assert + logger.Received(1).Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(errorMessage => + errorMessage.ToString().Contains("Warning invalid confirmed or accepted state") + && errorMessage.ToString().Contains(organizationUser.OrganizationId.ToString()) + && errorMessage.ToString().Contains(organizationUser.UserId.ToString()) + && errorMessage.ToString().Contains(organizationUser.Id.ToString()) + // Ensure that no PII is included in the log. + && !errorMessage.ToString().Contains(organizationUser.Email) + ), + null, + Arg.Any>() + ); + } + + + public static List ShouldNotLogTestCases => + [ + new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Accepted, Email = null } }, + new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Confirmed, Email = null } }, + new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Invited, Email = "someone@example.com" } }, + new object[] { new OrganizationUser { Status = OrganizationUserStatusType.Revoked, Email = null } }, + ]; + + [Theory] + [MemberData(nameof(ShouldNotLogTestCases))] + public void LogUserInviteStateDiagnostics_WhenStateAreValid_ShouldNotLog(OrganizationUser user) + { + var logger = Substitute.For(); + + // Act + logger.LogUserInviteStateDiagnostics(user); + + // Assert + logger.DidNotReceive().Log( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } +} From 899ff1b66093a71f0c690aa55a807b9f7a888fb2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:56:53 +0200 Subject: [PATCH 008/326] [deps] Tools: Update aws-sdk-net monorepo to v4 (#5874) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index e3141c7bfb..a6be8f484e 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 20bf1455cf2b67ba0d3e1fe4846a3d71856a4eaf Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:17:51 -0400 Subject: [PATCH 009/326] [PM-20348] Add pending auth request endpoint (#5957) * Feat(pm-20348): * Add migration scripts for Read Pending Auth Requests by UserId stored procedure and new `view` for pending AuthRequest. * View only returns the most recent pending authRequest, or none at all if the most recent is answered. * Implement stored procedure in AuthRequestRepository for both Dapper and Entity Framework. * Update AuthRequestController to query the new View to get a user's most recent pending auth requests response includes the requesting deviceId. * Doc: * Move summary xml comments to interface. * Added comments for the AuthRequestService. * Test: * Added testing for AuthRequestsController. * Added testing for repositories. * Added integration tests for multiple auth requests but only returning the most recent. --- .../Controllers/AuthRequestsController.cs | 40 +-- .../PendingAuthRequestResponseModel.cs | 15 + .../Models/Data/PendingAuthRequestDetails.cs | 83 ++++++ .../Repositories/IAuthRequestRepository.cs | 7 + src/Core/Auth/Services/IAuthRequestService.cs | 41 ++- .../Implementations/AuthRequestService.cs | 16 +- .../Repositories/AuthRequestRepository.cs | 23 +- .../Repositories/AuthRequestRepository.cs | 37 ++- .../AuthRequest_ReadPendingByUserId.sql | 12 + .../Views/AuthRequestPendingDetailsView.sql | 38 +++ test/Api.Test/Api.Test.csproj | 5 - .../AuthRequestsControllerTests.cs | 258 ++++++++++++++++++ .../AuthRequestRepositoryTests.cs | 174 +++++++++++- ...-00_AddReadPendingAuthRequestsByUserId.sql | 53 ++++ 14 files changed, 752 insertions(+), 50 deletions(-) create mode 100644 src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs create mode 100644 src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs create mode 100644 src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql create mode 100644 src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql create mode 100644 test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs create mode 100644 util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index f7edc7dec4..d1d6a8a524 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -1,5 +1,6 @@ using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; +using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.AuthRequest; using Bit.Core.Auth.Services; @@ -7,6 +8,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,31 +16,23 @@ namespace Bit.Api.Auth.Controllers; [Route("auth-requests")] [Authorize("Application")] -public class AuthRequestsController : Controller +public class AuthRequestsController( + IUserService userService, + IAuthRequestRepository authRequestRepository, + IGlobalSettings globalSettings, + IAuthRequestService authRequestService) : Controller { - private readonly IUserService _userService; - private readonly IAuthRequestRepository _authRequestRepository; - private readonly IGlobalSettings _globalSettings; - private readonly IAuthRequestService _authRequestService; - - public AuthRequestsController( - IUserService userService, - IAuthRequestRepository authRequestRepository, - IGlobalSettings globalSettings, - IAuthRequestService authRequestService) - { - _userService = userService; - _authRequestRepository = authRequestRepository; - _globalSettings = globalSettings; - _authRequestService = authRequestService; - } + private readonly IUserService _userService = userService; + private readonly IAuthRequestRepository _authRequestRepository = authRequestRepository; + private readonly IGlobalSettings _globalSettings = globalSettings; + private readonly IAuthRequestService _authRequestService = authRequestService; [HttpGet("")] public async Task> Get() { var userId = _userService.GetProperUserId(User).Value; var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId); - var responses = authRequests.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)).ToList(); + var responses = authRequests.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)); return new ListResponseModel(responses); } @@ -56,6 +50,16 @@ public class AuthRequestsController : Controller return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault); } + [HttpGet("pending")] + [RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)] + public async Task> GetPendingAuthRequestsAsync() + { + var userId = _userService.GetProperUserId(User).Value; + var rawResponse = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId); + var responses = rawResponse.Select(a => new PendingAuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)); + return new ListResponseModel(responses); + } + [HttpGet("{id}/response")] [AllowAnonymous] public async Task GetResponse(Guid id, [FromQuery] string code) diff --git a/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs new file mode 100644 index 0000000000..8428593068 --- /dev/null +++ b/src/Api/Auth/Models/Response/PendingAuthRequestResponseModel.cs @@ -0,0 +1,15 @@ +using Bit.Core.Auth.Models.Data; + +namespace Bit.Api.Auth.Models.Response; + +public class PendingAuthRequestResponseModel : AuthRequestResponseModel +{ + public PendingAuthRequestResponseModel(PendingAuthRequestDetails authRequest, string vaultUri, string obj = "auth-request") + : base(authRequest, vaultUri, obj) + { + ArgumentNullException.ThrowIfNull(authRequest); + RequestDeviceId = authRequest.RequestDeviceId; + } + + public Guid? RequestDeviceId { get; set; } +} diff --git a/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs b/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs new file mode 100644 index 0000000000..0755e941b7 --- /dev/null +++ b/src/Core/Auth/Models/Data/PendingAuthRequestDetails.cs @@ -0,0 +1,83 @@ + +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Enums; + +namespace Bit.Core.Auth.Models.Data; + +public class PendingAuthRequestDetails : AuthRequest +{ + public Guid? RequestDeviceId { get; set; } + + /** + * Constructor for EF response. + */ + public PendingAuthRequestDetails( + AuthRequest authRequest, + Guid? deviceId) + { + ArgumentNullException.ThrowIfNull(authRequest); + + Id = authRequest.Id; + UserId = authRequest.UserId; + OrganizationId = authRequest.OrganizationId; + Type = authRequest.Type; + RequestDeviceIdentifier = authRequest.RequestDeviceIdentifier; + RequestDeviceType = authRequest.RequestDeviceType; + RequestIpAddress = authRequest.RequestIpAddress; + RequestCountryName = authRequest.RequestCountryName; + ResponseDeviceId = authRequest.ResponseDeviceId; + AccessCode = authRequest.AccessCode; + PublicKey = authRequest.PublicKey; + Key = authRequest.Key; + MasterPasswordHash = authRequest.MasterPasswordHash; + Approved = authRequest.Approved; + CreationDate = authRequest.CreationDate; + ResponseDate = authRequest.ResponseDate; + AuthenticationDate = authRequest.AuthenticationDate; + RequestDeviceId = deviceId; + } + + /** + * Constructor for dapper response. + */ + public PendingAuthRequestDetails( + Guid id, + Guid userId, + Guid organizationId, + short type, + string requestDeviceIdentifier, + short requestDeviceType, + string requestIpAddress, + string requestCountryName, + Guid? responseDeviceId, + string accessCode, + string publicKey, + string key, + string masterPasswordHash, + bool? approved, + DateTime creationDate, + DateTime? responseDate, + DateTime? authenticationDate, + Guid deviceId) + { + Id = id; + UserId = userId; + OrganizationId = organizationId; + Type = (AuthRequestType)type; + RequestDeviceIdentifier = requestDeviceIdentifier; + RequestDeviceType = (DeviceType)requestDeviceType; + RequestIpAddress = requestIpAddress; + RequestCountryName = requestCountryName; + ResponseDeviceId = responseDeviceId; + AccessCode = accessCode; + PublicKey = publicKey; + Key = key; + MasterPasswordHash = masterPasswordHash; + Approved = approved; + CreationDate = creationDate; + ResponseDate = responseDate; + AuthenticationDate = authenticationDate; + RequestDeviceId = deviceId; + } +} diff --git a/src/Core/Auth/Repositories/IAuthRequestRepository.cs b/src/Core/Auth/Repositories/IAuthRequestRepository.cs index 3b01a452f9..7a66ad6e34 100644 --- a/src/Core/Auth/Repositories/IAuthRequestRepository.cs +++ b/src/Core/Auth/Repositories/IAuthRequestRepository.cs @@ -9,6 +9,13 @@ public interface IAuthRequestRepository : IRepository { Task DeleteExpiredAsync(TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration); Task> GetManyByUserIdAsync(Guid userId); + /// + /// Gets all active pending auth requests for a user. Each auth request in the collection will be associated with a different + /// device. It will be the most current request for the device. + /// + /// UserId of the owner of the AuthRequests + /// a collection Auth request details or empty + Task> GetManyPendingAuthRequestByUserId(Guid userId); Task> GetManyPendingByOrganizationIdAsync(Guid organizationId); Task> GetManyAdminApprovalRequestsByManyIdsAsync(Guid organizationId, IEnumerable ids); Task UpdateManyAsync(IEnumerable authRequests); diff --git a/src/Core/Auth/Services/IAuthRequestService.cs b/src/Core/Auth/Services/IAuthRequestService.cs index 4e057f0ccf..d81f6e7c8c 100644 --- a/src/Core/Auth/Services/IAuthRequestService.cs +++ b/src/Core/Auth/Services/IAuthRequestService.cs @@ -1,5 +1,9 @@ using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Exceptions; using Bit.Core.Auth.Models.Api.Request.AuthRequest; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Settings; #nullable enable @@ -7,8 +11,41 @@ namespace Bit.Core.Auth.Services; public interface IAuthRequestService { - Task GetAuthRequestAsync(Guid id, Guid userId); - Task GetValidatedAuthRequestAsync(Guid id, string code); + /// + /// Fetches an authRequest by Id. Returns AuthRequest if AuthRequest.UserId mateches + /// userId. Returns null if the user doesn't match or if the AuthRequest is not found. + /// + /// Authrequest Id being fetched + /// user who owns AuthRequest + /// An AuthRequest or null + Task GetAuthRequestAsync(Guid authRequestId, Guid userId); + /// + /// Fetches the authrequest from the database with the id provided. Then checks + /// the accessCode against the AuthRequest.AccessCode from the database. accessCodes + /// must match the found authRequest, and the AuthRequest must not be expired. Expiration + /// is configured in + /// + /// AuthRequest being acted on + /// Access code of the authrequest, must match saved database value + /// A valid AuthRequest or null + Task GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode); + /// + /// Validates and Creates an in the database, as well as pushes it through notifications services + /// + /// + /// This method can only be called inside of an HTTP call because of it's reliance on + /// Task CreateAuthRequestAsync(AuthRequestCreateRequestModel model); + /// + /// Updates the AuthRequest per the AuthRequestUpdateRequestModel context. This approves + /// or rejects the login request. + /// + /// AuthRequest being acted on. + /// User acting on AuthRequest + /// Update context for the AuthRequest + /// retuns an AuthRequest or throws an exception + /// Thows if the AuthRequest has already been Approved/Rejected + /// Throws if the AuthRequest as expired or the userId doesn't match + /// Throws if the device isn't associated with the UserId Task UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model); } diff --git a/src/Core/Auth/Services/Implementations/AuthRequestService.cs b/src/Core/Auth/Services/Implementations/AuthRequestService.cs index 0fd1846d00..11682b524f 100644 --- a/src/Core/Auth/Services/Implementations/AuthRequestService.cs +++ b/src/Core/Auth/Services/Implementations/AuthRequestService.cs @@ -58,9 +58,9 @@ public class AuthRequestService : IAuthRequestService _logger = logger; } - public async Task GetAuthRequestAsync(Guid id, Guid userId) + public async Task GetAuthRequestAsync(Guid authRequestId, Guid userId) { - var authRequest = await _authRequestRepository.GetByIdAsync(id); + var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId); if (authRequest == null || authRequest.UserId != userId) { return null; @@ -69,10 +69,10 @@ public class AuthRequestService : IAuthRequestService return authRequest; } - public async Task GetValidatedAuthRequestAsync(Guid id, string code) + public async Task GetValidatedAuthRequestAsync(Guid authRequestId, string accessCode) { - var authRequest = await _authRequestRepository.GetByIdAsync(id); - if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code)) + var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId); + if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, accessCode)) { return null; } @@ -85,12 +85,6 @@ public class AuthRequestService : IAuthRequestService return authRequest; } - /// - /// Validates and Creates an in the database, as well as pushes it through notifications services - /// - /// - /// This method can only be called inside of an HTTP call because of it's reliance on - /// public async Task CreateAuthRequestAsync(AuthRequestCreateRequestModel model) { if (!_currentContext.DeviceType.HasValue) diff --git a/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs index db6419d389..c9cf796986 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/AuthRequestRepository.cs @@ -14,13 +14,12 @@ namespace Bit.Infrastructure.Dapper.Auth.Repositories; public class AuthRequestRepository : Repository, IAuthRequestRepository { + private readonly GlobalSettings _globalSettings; public AuthRequestRepository(GlobalSettings globalSettings) - : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) - { } - - public AuthRequestRepository(string connectionString, string readOnlyConnectionString) - : base(connectionString, readOnlyConnectionString) - { } + : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + _globalSettings = globalSettings; + } public async Task DeleteExpiredAsync( TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration) @@ -52,6 +51,18 @@ public class AuthRequestRepository : Repository, IAuthRequest } } + public async Task> GetManyPendingAuthRequestByUserId(Guid userId) + { + var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes; + using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[AuthRequest_ReadPendingByUserId]", + new { UserId = userId, ExpirationMinutes = expirationMinutes }, + commandType: CommandType.StoredProcedure); + + return results; + } + public async Task> GetManyPendingByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs index 7dee40a9e6..23cdb60dcd 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/AuthRequestRepository.cs @@ -3,6 +3,7 @@ using AutoMapper.QueryableExtensions; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Settings; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; @@ -14,9 +15,13 @@ namespace Bit.Infrastructure.EntityFramework.Auth.Repositories; public class AuthRequestRepository : Repository, IAuthRequestRepository { - public AuthRequestRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) - : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.AuthRequests) - { } + private readonly IGlobalSettings _globalSettings; + public AuthRequestRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper, IGlobalSettings globalSettings) + : base(serviceScopeFactory, mapper, context => context.AuthRequests) + { + _globalSettings = globalSettings; + } + public async Task DeleteExpiredAsync( TimeSpan userRequestExpiration, TimeSpan adminRequestExpiration, TimeSpan afterAdminApprovalExpiration) { @@ -57,6 +62,32 @@ public class AuthRequestRepository : Repository> GetManyPendingAuthRequestByUserId(Guid userId) + { + var expirationMinutes = (int)_globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes; + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var mostRecentAuthRequests = await + (from authRequest in dbContext.AuthRequests + where authRequest.Type == AuthRequestType.AuthenticateAndUnlock + || authRequest.Type == AuthRequestType.Unlock + where authRequest.UserId == userId + where authRequest.CreationDate.AddMinutes(expirationMinutes) >= DateTime.UtcNow + group authRequest by authRequest.RequestDeviceIdentifier into groupedAuthRequests + select + (from r in groupedAuthRequests + join d in dbContext.Devices on new { r.RequestDeviceIdentifier, r.UserId } + equals new { RequestDeviceIdentifier = d.Identifier, d.UserId } into deviceJoin + from dj in deviceJoin.DefaultIfEmpty() // This creates a left join allowing null for devices + orderby r.CreationDate descending + select new PendingAuthRequestDetails(r, dj.Id)).First() + ).ToListAsync(); + + mostRecentAuthRequests.RemoveAll(a => a.Approved != null); + + return mostRecentAuthRequests; + } + public async Task> GetManyAdminApprovalRequestsByManyIdsAsync( Guid organizationId, IEnumerable ids) diff --git a/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql new file mode 100644 index 0000000000..4c3217812a --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByUserId.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId] + @UserId UNIQUEIDENTIFIER, + @ExpirationMinutes INT +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AuthRequestPendingDetailsView] + WHERE [UserId] = @UserId + AND [CreationDate] >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) +END diff --git a/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql b/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql new file mode 100644 index 0000000000..d0433bca09 --- /dev/null +++ b/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql @@ -0,0 +1,38 @@ +CREATE VIEW [dbo].[AuthRequestPendingDetailsView] +AS + WITH + PendingRequests + AS + ( + SELECT + [AR].*, + [D].[Id] AS [DeviceId], + ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier] ORDER BY [AR].[CreationDate] DESC) AS [rn] + FROM [dbo].[AuthRequest] [AR] + LEFT JOIN [dbo].[Device] [D] + ON [AR].[RequestDeviceIdentifier] = [D].[Identifier] + AND [D].[UserId] = [AR].[UserId] + WHERE [AR].[Type] IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock + ) + SELECT + [PR].[Id], + [PR].[UserId], + [PR].[OrganizationId], + [PR].[Type], + [PR].[RequestDeviceIdentifier], + [PR].[RequestDeviceType], + [PR].[RequestIpAddress], + [PR].[RequestCountryName], + [PR].[ResponseDeviceId], + [PR].[AccessCode], + [PR].[PublicKey], + [PR].[Key], + [PR].[MasterPasswordHash], + [PR].[Approved], + [PR].[CreationDate], + [PR].[ResponseDate], + [PR].[AuthenticationDate], + [PR].[DeviceId] + FROM [PendingRequests] [PR] + WHERE [PR].[rn] = 1 + AND [PR].[Approved] IS NULL -- since we only want pending requests we only want the most recent that is also approved = null diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index d6b31ce930..fb75246d4f 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -25,9 +25,4 @@ - - - - - diff --git a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs new file mode 100644 index 0000000000..828911f6bd --- /dev/null +++ b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs @@ -0,0 +1,258 @@ +using System.Security.Claims; +using Bit.Api.Auth.Controllers; +using Bit.Api.Auth.Models.Response; +using Bit.Api.Models.Response; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Api.Request.AuthRequest; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Services; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Auth.Controllers; + +[ControllerCustomize(typeof(AuthRequestsController))] +[SutProviderCustomize] +public class AuthRequestsControllerTests +{ + const string _testGlobalSettingsBaseUri = "https://vault.test.dev"; + + [Theory, BitAutoData] + public async Task Get_ReturnsExpectedResult( + SutProvider sutProvider, + User user, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns([authRequest]); + + // Act + var result = await sutProvider.Sut.Get(); + + // Assert + Assert.NotNull(result); + var expectedCount = 1; + Assert.Equal(result.Data.Count(), expectedCount); + Assert.IsType>(result); + } + + [Theory, BitAutoData] + public async Task GetById_ThrowsNotFoundException( + SutProvider sutProvider, + User user, + AuthRequest authRequest) + { + // Arrange + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .GetAuthRequestAsync(authRequest.Id, user.Id) + .Returns((AuthRequest)null); + + // Act + // Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.Get(authRequest.Id)); + } + + [Theory, BitAutoData] + public async Task GetById_ReturnsAuthRequest( + SutProvider sutProvider, + User user, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .GetAuthRequestAsync(authRequest.Id, user.Id) + .Returns(authRequest); + + // Act + var result = await sutProvider.Sut.Get(authRequest.Id); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task GetPending_ReturnsExpectedResult( + SutProvider sutProvider, + User user, + PendingAuthRequestDetails authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .GetManyPendingAuthRequestByUserId(user.Id) + .Returns([authRequest]); + + // Act + var result = await sutProvider.Sut.GetPendingAuthRequestsAsync(); + + // Assert + Assert.NotNull(result); + var expectedCount = 1; + Assert.Equal(result.Data.Count(), expectedCount); + Assert.IsType>(result); + } + + [Theory, BitAutoData] + public async Task GetResponseById_ThrowsNotFoundException( + SutProvider sutProvider, + AuthRequest authRequest) + { + // Arrange + sutProvider.GetDependency() + .GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode) + .Returns((AuthRequest)null); + + // Act + // Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode)); + } + + [Theory, BitAutoData] + public async Task GetResponseById_ReturnsAuthRequest( + SutProvider sutProvider, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + sutProvider.GetDependency() + .GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode) + .Returns(authRequest); + + // Act + var result = await sutProvider.Sut.GetResponse(authRequest.Id, authRequest.AccessCode); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task Post_AdminApprovalRequest_ThrowsBadRequestException( + SutProvider sutProvider, + AuthRequestCreateRequestModel authRequest) + { + // Arrange + authRequest.Type = AuthRequestType.AdminApproval; + + // Act + // Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.Post(authRequest)); + + var expectedMessage = "You must be authenticated to create a request of that type."; + Assert.Equal(exception.Message, expectedMessage); + } + + [Theory, BitAutoData] + public async Task Post_ReturnsAuthRequest( + SutProvider sutProvider, + AuthRequestCreateRequestModel requestModel, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + requestModel.Type = AuthRequestType.AuthenticateAndUnlock; + sutProvider.GetDependency() + .CreateAuthRequestAsync(requestModel) + .Returns(authRequest); + + // Act + var result = await sutProvider.Sut.Post(requestModel); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PostAdminRequest_ReturnsAuthRequest( + SutProvider sutProvider, + AuthRequestCreateRequestModel requestModel, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + requestModel.Type = AuthRequestType.AuthenticateAndUnlock; + sutProvider.GetDependency() + .CreateAuthRequestAsync(requestModel) + .Returns(authRequest); + + // Act + var result = await sutProvider.Sut.PostAdminRequest(requestModel); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task Put_ReturnsAuthRequest( + SutProvider sutProvider, + User user, + AuthRequestUpdateRequestModel requestModel, + AuthRequest authRequest) + { + // Arrange + SetBaseServiceUri(sutProvider); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .UpdateAuthRequestAsync(authRequest.Id, user.Id, requestModel) + .Returns(authRequest); + + // Act + var result = await sutProvider.Sut + .Put(authRequest.Id, requestModel); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + private void SetBaseServiceUri(SutProvider sutProvider) + { + sutProvider.GetDependency() + .BaseServiceUri + .Vault + .Returns(_testGlobalSettingsBaseUri); + } +} diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/AuthRequestRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/AuthRequestRepositoryTests.cs index 8cd8cb607c..56f01748fb 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/AuthRequestRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/AuthRequestRepositoryTests.cs @@ -66,10 +66,8 @@ public class AuthRequestRepositoryTests Assert.NotNull(await authRequestRepository.GetByIdAsync(notExpiredAdminApprovalRequest.Id)); Assert.NotNull(await authRequestRepository.GetByIdAsync(notExpiredApprovedAdminApprovalRequest.Id)); - // Ensure the repository responds with the amount of items it deleted and it deleted the right amount. - // NOTE: On local development this might fail on it's first run because the developer could have expired AuthRequests - // on their machine but aren't running the job that would delete them. The second run of this test should succeed. - Assert.Equal(4, numberOfDeleted); + // Ensure the repository responds with the amount of items it deleted and it deleted the right amount, which could include other auth requests from other tests so we take the minimum known acceptable amount. + Assert.True(numberOfDeleted >= 4); } [DatabaseTheory, DatabaseData] @@ -182,7 +180,157 @@ public class AuthRequestRepositoryTests Assert.Null(uncreatedAuthRequest); } - private static AuthRequest CreateAuthRequest(Guid userId, AuthRequestType authRequestType, DateTime creationDate, bool? approved = null, DateTime? responseDate = null) + /// + /// Test to determine that when no valid authRequest exists in the database the return value is null. + /// + [DatabaseTheory, DatabaseData] + public async Task GetManyPendingAuthRequestByUserId_AuthRequestsInvalid_ReturnsEmptyEnumerable_Success( + IAuthRequestRepository authRequestRepository, + IUserRepository userRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + List authRequests = []; + + // A user auth request type that has passed its expiration time, should not be returned. + var authRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + CreateExpiredDate(_userRequestExpiration)); + authRequest.RequestDeviceIdentifier = "auth_request_expired"; + authRequests.Add(await authRequestRepository.CreateAsync(authRequest)); + + // A valid time AuthRequest but for pending we do not fetch admin auth requests + authRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AdminApproval, + DateTime.UtcNow.AddMinutes(-1)); + authRequest.RequestDeviceIdentifier = "admin_auth_request"; + authRequests.Add(await authRequestRepository.CreateAsync(authRequest)); + + // A valid time AuthRequest but the request has been approved/rejected, so it should not be returned. + authRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-1), + false); + authRequest.RequestDeviceIdentifier = "approved_auth_request"; + authRequests.Add(await authRequestRepository.CreateAsync(authRequest)); + + var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id); + Assert.NotNull(result); + Assert.Empty(result); + + // Verify that there are authRequests associated with the user. + Assert.NotEmpty(await authRequestRepository.GetManyByUserIdAsync(user.Id)); + + await CleanupTestAsync(authRequests, authRequestRepository); + } + + /// + /// Test to determine that when multiple valid authRequest exist for a device only the soonest one is returned. + /// + [DatabaseTheory, DatabaseData] + public async Task GetManyPendingAuthRequestByUserId_MultipleRequestForSingleDevice_ReturnsMostRecent( + IAuthRequestRepository authRequestRepository, + IUserRepository userRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var oneMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-1)); + oneMinuteOldAuthRequest = await authRequestRepository.CreateAsync(oneMinuteOldAuthRequest); + + var fiveMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-5)); + fiveMinuteOldAuthRequest = await authRequestRepository.CreateAsync(fiveMinuteOldAuthRequest); + + var tenMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-10)); + tenMinuteOldAuthRequest = await authRequestRepository.CreateAsync(tenMinuteOldAuthRequest); + + var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id); + Assert.NotNull(result); + // since we group by device there should only be a single return since the device Id is the same + Assert.Single(result); + var resultAuthRequest = result.First(); + Assert.Equal(oneMinuteOldAuthRequest.Id, resultAuthRequest.Id); + + List authRequests = [oneMinuteOldAuthRequest, fiveMinuteOldAuthRequest, tenMinuteOldAuthRequest]; + + await CleanupTestAsync(authRequests, authRequestRepository); + } + + /// + /// Test to determine that when multiple authRequests exist for a device if the most recent is approved then + /// there should be no return. + /// + [DatabaseTheory, DatabaseData] + public async Task GetManyPendingAuthRequestByUserId_MultipleRequestForSingleDevice_MostRecentIsApproved_ReturnsEmpty( + IAuthRequestRepository authRequestRepository, + IUserRepository userRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + // approved auth request + var oneMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-1), + false); + oneMinuteOldAuthRequest = await authRequestRepository.CreateAsync(oneMinuteOldAuthRequest); + + var fiveMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-5)); + fiveMinuteOldAuthRequest = await authRequestRepository.CreateAsync(fiveMinuteOldAuthRequest); + + var tenMinuteOldAuthRequest = CreateAuthRequest( + user.Id, + AuthRequestType.AuthenticateAndUnlock, + DateTime.UtcNow.AddMinutes(-10)); + tenMinuteOldAuthRequest = await authRequestRepository.CreateAsync(tenMinuteOldAuthRequest); + + var result = await authRequestRepository.GetManyPendingAuthRequestByUserId(user.Id); + Assert.NotNull(result); + // result should be empty since the most recent request was addressed + Assert.Empty(result); + + List authRequests = [oneMinuteOldAuthRequest, fiveMinuteOldAuthRequest, tenMinuteOldAuthRequest]; + await CleanupTestAsync(authRequests, authRequestRepository); + } + + private static AuthRequest CreateAuthRequest( + Guid userId, + AuthRequestType authRequestType, + DateTime creationDate, + bool? approved = null, + DateTime? responseDate = null) { return new AuthRequest { @@ -203,4 +351,20 @@ public class AuthRequestRepositoryTests var exp = expirationPeriod + TimeSpan.FromMinutes(1); return DateTime.UtcNow.Add(exp.Negate()); } + + /// + /// Cleans up the test data created by the test methods. This supports the DeleteExpiredAsync Test. + /// + /// Created Auth Requests + /// repository context for the current test + /// void + private static async Task CleanupTestAsync( + IEnumerable authRequests, + IAuthRequestRepository authRequestRepository) + { + foreach (var authRequest in authRequests) + { + await authRequestRepository.DeleteAsync(authRequest); + } + } } diff --git a/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql b/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql new file mode 100644 index 0000000000..86f4683cff --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-04-00_AddReadPendingAuthRequestsByUserId.sql @@ -0,0 +1,53 @@ +CREATE OR ALTER VIEW [dbo].[AuthRequestPendingDetailsView] +AS + WITH + PendingRequests + AS + ( + SELECT + [AR].*, + [D].[Id] AS [DeviceId], + ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier] ORDER BY [AR].[CreationDate] DESC) AS [rn] + FROM [dbo].[AuthRequest] [AR] + LEFT JOIN [dbo].[Device] [D] + ON [AR].[RequestDeviceIdentifier] = [D].[Identifier] + AND [D].[UserId] = [AR].[UserId] + WHERE [AR].[Type] IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock + ) + SELECT + [PR].[Id], + [PR].[UserId], + [PR].[OrganizationId], + [PR].[Type], + [PR].[RequestDeviceIdentifier], + [PR].[RequestDeviceType], + [PR].[RequestIpAddress], + [PR].[RequestCountryName], + [PR].[ResponseDeviceId], + [PR].[AccessCode], + [PR].[PublicKey], + [PR].[Key], + [PR].[MasterPasswordHash], + [PR].[Approved], + [PR].[CreationDate], + [PR].[ResponseDate], + [PR].[AuthenticationDate], + [PR].[DeviceId] + FROM [PendingRequests] [PR] + WHERE [PR].[rn] = 1 + AND [PR].[Approved] IS NULL -- since we only want pending requests we only want the most recent that is also approved = null + GO + +CREATE OR ALTER PROCEDURE [dbo].[AuthRequest_ReadPendingByUserId] + @UserId UNIQUEIDENTIFIER, + @ExpirationMinutes INT +AS +BEGIN + SET NOCOUNT ON + + SELECT * + FROM [dbo].[AuthRequestPendingDetailsView] + WHERE [UserId] = @UserId + AND [CreationDate] >= DATEADD(MINUTE, -@ExpirationMinutes, GETUTCDATE()) +END + GO From e8ad23c8bc5a06fbdc3915f1e7c63c9d618e8da9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:17:53 +0100 Subject: [PATCH 010/326] [PM-22442] Remove CollectionService (#6015) * Refactor Collections and OrganizationExport Controllers to Remove ICollectionService Dependency * Remove ICollectionService registration from ServiceCollectionExtensions * Remove CollectionServiceTests file as part of the ongoing refactor to eliminate ICollectionService. * Remove ICollectionService and its implementation in CollectionService as part of the ongoing refactor to eliminate the service. --- src/Api/Controllers/CollectionsController.cs | 3 - .../OrganizationExportController.cs | 3 - src/Core/Services/ICollectionService.cs | 8 --- .../Implementations/CollectionService.cs | 40 ----------- .../Utilities/ServiceCollectionExtensions.cs | 1 - .../Services/CollectionServiceTests.cs | 70 ------------------- 6 files changed, 125 deletions(-) delete mode 100644 src/Core/Services/ICollectionService.cs delete mode 100644 src/Core/Services/Implementations/CollectionService.cs delete mode 100644 test/Core.Test/Services/CollectionServiceTests.cs diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index c8a12b9c22..87f53b0891 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -19,7 +19,6 @@ namespace Bit.Api.Controllers; public class CollectionsController : Controller { private readonly ICollectionRepository _collectionRepository; - private readonly ICollectionService _collectionService; private readonly ICreateCollectionCommand _createCollectionCommand; private readonly IUpdateCollectionCommand _updateCollectionCommand; private readonly IDeleteCollectionCommand _deleteCollectionCommand; @@ -30,7 +29,6 @@ public class CollectionsController : Controller public CollectionsController( ICollectionRepository collectionRepository, - ICollectionService collectionService, ICreateCollectionCommand createCollectionCommand, IUpdateCollectionCommand updateCollectionCommand, IDeleteCollectionCommand deleteCollectionCommand, @@ -40,7 +38,6 @@ public class CollectionsController : Controller IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand) { _collectionRepository = collectionRepository; - _collectionService = collectionService; _createCollectionCommand = createCollectionCommand; _updateCollectionCommand = updateCollectionCommand; _deleteCollectionCommand = deleteCollectionCommand; diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index 520746f139..10b2d87456 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -19,7 +19,6 @@ public class OrganizationExportController : Controller { private readonly ICurrentContext _currentContext; private readonly IUserService _userService; - private readonly ICollectionService _collectionService; private readonly ICipherService _cipherService; private readonly GlobalSettings _globalSettings; private readonly IFeatureService _featureService; @@ -30,7 +29,6 @@ public class OrganizationExportController : Controller public OrganizationExportController( ICurrentContext currentContext, ICipherService cipherService, - ICollectionService collectionService, IUserService userService, GlobalSettings globalSettings, IFeatureService featureService, @@ -40,7 +38,6 @@ public class OrganizationExportController : Controller { _currentContext = currentContext; _cipherService = cipherService; - _collectionService = collectionService; _userService = userService; _globalSettings = globalSettings; _featureService = featureService; diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs deleted file mode 100644 index 101f3ea23b..0000000000 --- a/src/Core/Services/ICollectionService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Bit.Core.Entities; - -namespace Bit.Core.Services; - -public interface ICollectionService -{ - Task DeleteUserAsync(Collection collection, Guid organizationUserId); -} diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs deleted file mode 100644 index 3b828955af..0000000000 --- a/src/Core/Services/Implementations/CollectionService.cs +++ /dev/null @@ -1,40 +0,0 @@ -#nullable enable - -using Bit.Core.Entities; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; - -namespace Bit.Core.Services; - -public class CollectionService : ICollectionService -{ - private readonly IEventService _eventService; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly ICollectionRepository _collectionRepository; - - public CollectionService( - IEventService eventService, - IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository) - { - _eventService = eventService; - _organizationUserRepository = organizationUserRepository; - _collectionRepository = collectionRepository; - } - - public async Task DeleteUserAsync(Collection collection, Guid organizationUserId) - { - if (collection.Type == Enums.CollectionType.DefaultUserCollection) - { - throw new BadRequestException("You cannot modify member access for collections with the type as DefaultUserCollection."); - } - - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null || orgUser.OrganizationId != collection.OrganizationId) - { - throw new NotFoundException(); - } - await _collectionRepository.DeleteUserAsync(collection.Id, organizationUserId); - await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated); - } -} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 1c4473674c..1e5af42576 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -117,7 +117,6 @@ public static class ServiceCollectionExtensions services.AddTrialInitiationServices(); services.AddOrganizationServices(globalSettings); services.AddPolicyServices(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs deleted file mode 100644 index 118c0fa6b2..0000000000 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -[OrganizationCustomize] -public class CollectionServiceTest -{ - [Theory, BitAutoData] - public async Task DeleteUserAsync_DeletesValidUserWhoBelongsToCollection(Collection collection, - Organization organization, OrganizationUser organizationUser, SutProvider sutProvider) - { - collection.OrganizationId = organization.Id; - organizationUser.OrganizationId = organization.Id; - sutProvider.GetDependency().GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - await sutProvider.Sut.DeleteUserAsync(collection, organizationUser.Id); - - await sutProvider.GetDependency().Received() - .DeleteUserAsync(collection.Id, organizationUser.Id); - await sutProvider.GetDependency().Received().LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Updated); - } - - [Theory, BitAutoData] - public async Task DeleteUserAsync_InvalidUser_ThrowsNotFound(Collection collection, Organization organization, - OrganizationUser organizationUser, SutProvider sutProvider) - { - collection.OrganizationId = organization.Id; - sutProvider.GetDependency().GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - // user not in organization - await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(collection, organizationUser.Id)); - // invalid user - await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteUserAsync(collection, Guid.NewGuid())); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(default, default); - } - - [Theory, BitAutoData] - public async Task DeleteUserAsync_WithDefaultUserCollectionType_ThrowsBadRequest(Collection collection, - Organization organization, OrganizationUser organizationUser, SutProvider sutProvider) - { - collection.Type = CollectionType.DefaultUserCollection; - collection.OrganizationId = organization.Id; - organizationUser.OrganizationId = organization.Id; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(collection, organizationUser.Id)); - Assert.Contains("You cannot modify member access for collections with the type as DefaultUserCollection.", exception.Message); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(default, default); - } -} From f6cd661e8e628690c2d61d61070474e0a78c126b Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:52:38 -0400 Subject: [PATCH 011/326] [PM-17562] Add HEC integration support (#6010) * [PM-17562] Add HEC integration support * Re-ordered parameters per PR suggestion * Apply suggestions from code review Co-authored-by: Matt Bishop * Refactored webhook request model validation to be more clear --------- Co-authored-by: Matt Bishop --- dev/servicebusemulator_config.json | 17 ++++ ...ionIntegrationConfigurationRequestModel.cs | 4 + .../OrgnizationIntegrationRequestModel.cs | 36 +++++++- .../AdminConsole/Enums/IntegrationType.cs | 3 + .../Data/EventIntegrations/HecIntegration.cs | 5 ++ .../EventIntegrations/WebhookIntegration.cs | 5 ++ .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 2 +- .../EventIntegrations/README.md | 27 ++++-- .../WebhookIntegrationHandler.cs | 4 +- .../Utilities/IntegrationTemplateProcessor.cs | 12 ++- src/Core/Settings/GlobalSettings.cs | 5 ++ .../Utilities/ServiceCollectionExtensions.cs | 14 ++++ ...ntegrationsConfigurationControllerTests.cs | 12 +-- ...tegrationConfigurationRequestModelTests.cs | 84 ++++++++++++++----- ...rganizationIntegrationRequestModelTests.cs | 66 ++++++++++++++- .../IntegrationMessageTests.cs | 4 +- ...ionIntegrationConfigurationDetailsTests.cs | 16 ++++ .../Services/EventIntegrationHandlerTests.cs | 20 ++--- .../Services/IntegrationHandlerTests.cs | 2 +- .../WebhookIntegrationHandlerTests.cs | 18 ++-- .../IntegrationTemplateProcessorTests.cs | 11 +++ 22 files changed, 302 insertions(+), 67 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index b107bc6190..dcf48b7a8c 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -31,6 +31,9 @@ }, { "Name": "events-webhook-subscription" + }, + { + "Name": "events-hec-subscription" } ] }, @@ -64,6 +67,20 @@ } } ] + }, + { + "Name": "integration-hec-subscription", + "Rules": [ + { + "Name": "hec-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "hec" + } + } + } + ] } ] } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index d4d69f77c1..c6dfb49ef3 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -33,6 +33,10 @@ public class OrganizationIntegrationConfigurationRequestModel return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid() && IsFiltersValid(); + case IntegrationType.Hec: + 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 1a5c110254..edae0719e3 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; #nullable enable @@ -39,10 +41,22 @@ public class OrganizationIntegrationRequestModel : IValidatableObject yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) }); break; case IntegrationType.Webhook: - if (Configuration is not null) + if (string.IsNullOrWhiteSpace(Configuration)) + { + break; + } + if (!IsIntegrationValid()) { yield return new ValidationResult( - "Webhook integrations must not include configuration.", + "Webhook integrations must include valid configuration.", + new[] { nameof(Configuration) }); + } + break; + case IntegrationType.Hec: + if (!IsIntegrationValid()) + { + yield return new ValidationResult( + "HEC integrations must include valid configuration.", new[] { nameof(Configuration) }); } break; @@ -53,4 +67,22 @@ public class OrganizationIntegrationRequestModel : IValidatableObject break; } } + + private bool IsIntegrationValid() + { + if (string.IsNullOrWhiteSpace(Configuration)) + { + return false; + } + + try + { + var config = JsonSerializer.Deserialize(Configuration); + return config is not null; + } + catch + { + return false; + } + } } diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs index 5edd54df23..58e55193dc 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -6,6 +6,7 @@ public enum IntegrationType : int Scim = 2, Slack = 3, Webhook = 4, + Hec = 5 } public static class IntegrationTypeExtensions @@ -18,6 +19,8 @@ public static class IntegrationTypeExtensions return "slack"; case IntegrationType.Webhook: return "webhook"; + case IntegrationType.Hec: + return "hec"; default: throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}"); } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs new file mode 100644 index 0000000000..472ca70c0c --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs @@ -0,0 +1,5 @@ +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record HecIntegration(Uri Uri, string Scheme, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs new file mode 100644 index 0000000000..84b4b97857 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs @@ -0,0 +1,5 @@ +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index ff28edc301..2f5e8d29c1 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record WebhookIntegrationConfiguration(string Url, string? Scheme = null, string? Token = null); +public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index e0ed5dfcfa..4fa1a67c8e 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null); +public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index 7320f8bab7..b2327b0f75 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -197,22 +197,37 @@ interface and therefore can also handle directly all the message publishing func Organizations can configure integration configurations to send events to different endpoints -- each handler maps to a specific integration and checks for the configuration when it receives an event. -Currently, there are integrations / handlers for Slack and webhooks (as mentioned above). +Currently, there are integrations / handlers for Slack, webhooks, and HTTP Event Collector (HEC). ### `OrganizationIntegration` - 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 `null`. However, even though there is no configuration, an organization must - have a webhook `OrganizationIntegration` to enable configuration via `OrganizationIntegrationConfiguration`. + - 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" + } +``` ### `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 Webhook, this is the URL the request should be sent to: `{ "url": "https://api.example.com" }` + - 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. - `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 @@ -225,6 +240,8 @@ Currently, there are integrations / handlers for Slack and webhooks (as mentione - This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into a single object. The combined contents tell the integration's handler all the details needed to send to an external service. +- `OrganizationIntegrationConfiguration` takes precedence over `OrganizationIntegration` - any keys present in + both will receive the value declared in `OrganizationIntegrationConfiguration`. - An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from the database to determine what to publish at the integration level. diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index b66df59a69..99cad65efa 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -6,8 +6,6 @@ using System.Net.Http.Headers; using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -#nullable enable - namespace Bit.Core.Services; public class WebhookIntegrationHandler( @@ -21,7 +19,7 @@ public class WebhookIntegrationHandler( public override async Task HandleAsync(IntegrationMessage message) { - var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url); + var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri); request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); if (!string.IsNullOrEmpty(message.Configuration.Scheme)) { diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index aab4e448e5..dceeea85f4 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Text.Json; using System.Text.RegularExpressions; namespace Bit.Core.AdminConsole.Utilities; @@ -19,8 +20,15 @@ public static partial class IntegrationTemplateProcessor return TokenRegex().Replace(template, match => { var propertyName = match.Groups[1].Value; - var property = type.GetProperty(propertyName); - return property?.GetValue(values)?.ToString() ?? match.Value; + if (propertyName == "EventMessage") + { + return JsonSerializer.Serialize(values); + } + else + { + var property = type.GetProperty(propertyName); + return property?.GetValue(values)?.ToString() ?? match.Value; + } }); } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 7a794ec3f6..ba6d4e692e 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -297,6 +297,8 @@ public class GlobalSettings : IGlobalSettings public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription"; public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription"; public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription"; + public virtual string HecEventSubscriptionName { get; set; } = "events-hec-subscription"; + public virtual string HecIntegrationSubscriptionName { get; set; } = "integration-hec-subscription"; public string ConnectionString { @@ -336,6 +338,9 @@ public class GlobalSettings : IGlobalSettings public virtual string WebhookEventsQueueName { get; set; } = "events-webhook-queue"; public virtual string WebhookIntegrationQueueName { get; set; } = "integration-webhook-queue"; public virtual string WebhookIntegrationRetryQueueName { get; set; } = "integration-webhook-retry-queue"; + public virtual string HecEventsQueueName { get; set; } = "events-hec-queue"; + public virtual string HecIntegrationQueueName { get; set; } = "integration-hec-queue"; + public virtual string HecIntegrationRetryQueueName { get; set; } = "integration-hec-retry-queue"; public string HostName { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 1e5af42576..8534fc0d5c 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -666,6 +666,13 @@ public static class ServiceCollectionExtensions integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName, integrationType: IntegrationType.Webhook, globalSettings: globalSettings); + + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName, + integrationType: IntegrationType.Hec, + globalSettings: globalSettings); + return services; } @@ -755,6 +762,13 @@ public static class ServiceCollectionExtensions globalSettings.EventLogging.RabbitMq.MaxRetries, IntegrationType.Webhook); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.HecEventsQueueName, + globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.MaxRetries, + IntegrationType.Hec); + return services; } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index 4732ddd748..e2ee854793 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -189,7 +189,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; model.Filters = null; @@ -227,7 +227,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; model.Filters = null; @@ -390,7 +390,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = null; @@ -477,7 +477,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; model.Filters = null; @@ -520,7 +520,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; model.Filters = null; @@ -561,7 +561,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; model.Filters = null; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 20831ec7d9..6af5b8039b 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -17,13 +17,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests Template = "template" }; - Assert.False(model.IsValidForType(IntegrationType.CloudBillingSync)); + Assert.False(condition: model.IsValidForType(IntegrationType.CloudBillingSync)); } [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] + [InlineData(data: null)] + [InlineData(data: "")] + [InlineData(data: " ")] public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config) { var model = new OrganizationIntegrationConfigurationRequestModel @@ -32,25 +32,55 @@ public class OrganizationIntegrationConfigurationRequestModelTests Template = "template" }; - var result = model.IsValidForType(IntegrationType.Slack); - - Assert.False(result); + Assert.False(condition: model.IsValidForType(IntegrationType.Slack)); + Assert.False(condition: model.IsValidForType(IntegrationType.Webhook)); } [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] + [InlineData(data: "")] + [InlineData(data: " ")] + public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config) + { + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Template = "template" + }; + + Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); + } + + [Fact] + public void IsValidForType_NullHecConfiguration_ReturnsTrue() + { + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = null, + Template = "template" + }; + + Assert.True(condition: model.IsValidForType(IntegrationType.Hec)); + } + + [Theory] + [InlineData(data: null)] + [InlineData(data: "")] + [InlineData(data: " ")] public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template) { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration( + Uri: new Uri("https://localhost"), + Scheme: "Bearer", + Token: "AUTH-TOKEN")); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, Template = template }; - Assert.False(model.IsValidForType(IntegrationType.Webhook)); + Assert.False(condition: model.IsValidForType(IntegrationType.Slack)); + Assert.False(condition: model.IsValidForType(IntegrationType.Webhook)); + Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); } [Fact] @@ -62,14 +92,16 @@ public class OrganizationIntegrationConfigurationRequestModelTests Template = "template" }; - Assert.False(model.IsValidForType(IntegrationType.Webhook)); + Assert.False(condition: model.IsValidForType(IntegrationType.Slack)); + Assert.False(condition: model.IsValidForType(IntegrationType.Webhook)); + Assert.False(condition: model.IsValidForType(IntegrationType.Hec)); } [Fact] public void IsValidForType_InvalidJsonFilters_ReturnsFalse() { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration(Uri: new Uri("https://example.com"))); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, @@ -89,13 +121,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests Template = "template" }; - Assert.False(model.IsValidForType(IntegrationType.Scim)); + Assert.False(condition: model.IsValidForType(IntegrationType.Scim)); } [Fact] public void IsValidForType_ValidSlackConfiguration_ReturnsTrue() { - var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345")); + var config = JsonSerializer.Serialize(value: new SlackIntegrationConfiguration(ChannelId: "C12345")); var model = new OrganizationIntegrationConfigurationRequestModel { @@ -103,7 +135,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests Template = "template" }; - Assert.True(model.IsValidForType(IntegrationType.Slack)); + Assert.True(condition: model.IsValidForType(IntegrationType.Slack)); } [Fact] @@ -136,33 +168,39 @@ public class OrganizationIntegrationConfigurationRequestModelTests [Fact] public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue() { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); + var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"))); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, Template = "template" }; - Assert.True(model.IsValidForType(IntegrationType.Webhook)); + Assert.True(condition: model.IsValidForType(IntegrationType.Webhook)); } [Fact] public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue() { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration( + Uri: new Uri("https://localhost"), + Scheme: "Bearer", + Token: "AUTH-TOKEN")); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, Template = "template" }; - Assert.True(model.IsValidForType(IntegrationType.Webhook)); + Assert.True(condition: model.IsValidForType(IntegrationType.Webhook)); } [Fact] public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue() { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration( + Uri: new Uri("https://example.com"), + Scheme: "Bearer", + Token: "AUTH-TOKEN")); var filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { AndOperator = true, @@ -197,6 +235,6 @@ public class OrganizationIntegrationConfigurationRequestModelTests var unknownType = (IntegrationType)999; - Assert.False(model.IsValidForType(unknownType)); + Assert.False(condition: model.IsValidForType(unknownType)); } } diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs index fc9b399abd..147564dd94 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; +using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Xunit; @@ -70,7 +72,7 @@ public class OrganizationIntegrationRequestModelTests } [Fact] - public void Validate_Webhook_WithConfiguration_ReturnsConfigurationError() + public void Validate_Webhook_WithInvalidConfiguration_ReturnsConfigurationError() { var model = new OrganizationIntegrationRequestModel { @@ -82,7 +84,67 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must not include configuration", results[0].ErrorMessage); + Assert.Contains("must include valid configuration", results[0].ErrorMessage); + } + + [Fact] + public void Validate_Webhook_WithValidConfiguration_ReturnsNoErrors() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Webhook, + Configuration = JsonSerializer.Serialize(new WebhookIntegration(new Uri("https://example.com"))) + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Empty(results); + } + + [Fact] + public void Validate_Hec_WithNullConfiguration_ReturnsError() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Hec, + Configuration = null + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Single(results); + Assert.Contains(nameof(model.Configuration), results[0].MemberNames); + Assert.Contains("must include valid configuration", results[0].ErrorMessage); + } + + [Fact] + public void Validate_Hec_WithInvalidConfiguration_ReturnsError() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Hec, + Configuration = "Not valid" + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Single(results); + Assert.Contains(nameof(model.Configuration), results[0].MemberNames); + Assert.Contains("must include valid configuration", results[0].ErrorMessage); + } + + [Fact] + public void Validate_Hec_WithValidConfiguration_ReturnsNoErrors() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Hec, + Configuration = JsonSerializer.Serialize(new HecIntegration(Uri: new Uri("http://localhost"), Scheme: "Bearer", Token: "Token")) + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Empty(results); } [Fact] diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs index a68bfd4fcb..edd5cd488f 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs @@ -14,7 +14,7 @@ public class IntegrationMessageTests { var message = new IntegrationMessage { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), + Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = _messageId, RetryCount = 2, RenderedTemplate = string.Empty, @@ -34,7 +34,7 @@ public class IntegrationMessageTests { var message = new IntegrationMessage { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), + Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = _messageId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, diff --git a/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs b/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs index 99a11903b4..4b8cd4f47c 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs @@ -22,6 +22,22 @@ public class OrganizationIntegrationConfigurationDetailsTests Assert.Equal(expected, result.ToJsonString()); } + [Fact] + public void MergedConfiguration_WithSameKeyIndConfigAndIntegration_GivesPrecedenceToConfiguration() + { + var config = new { config = "A new config value" }; + var integration = new { config = "An integration value" }; + var expectedObj = new { config = "A new config value" }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = JsonSerializer.Serialize(config); + sut.IntegrationConfiguration = JsonSerializer.Serialize(integration); + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } + [Fact] public void MergedConfiguration_WithInvalidJsonConfigAndIntegration_ReturnsEmptyJson() { diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index e4ffabf691..2d1099db65 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -23,8 +23,8 @@ public class EventIntegrationHandlerTests private const string _templateWithOrganization = "Org: #OrganizationName#"; private const string _templateWithUser = "#UserName#, #UserEmail#"; private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; - private const string _url = "https://localhost"; - private const string _url2 = "https://example.com"; + 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(); private readonly ILogger> _logger = Substitute.For>>(); @@ -50,7 +50,7 @@ public class EventIntegrationHandlerTests { IntegrationType = IntegrationType.Webhook, MessageId = "TestMessageId", - Configuration = new WebhookIntegrationConfigurationDetails(_url), + Configuration = new WebhookIntegrationConfigurationDetails(_uri), RenderedTemplate = template, RetryCount = 0, DelayUntilDate = null @@ -66,7 +66,7 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = template; return [config]; @@ -76,11 +76,11 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = template; var config2 = Substitute.For(); config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 }); + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 }); config2.Template = template; return [config, config2]; @@ -90,7 +90,7 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; config.Filters = "Invalid Configuration!"; @@ -101,7 +101,7 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { }); @@ -149,7 +149,7 @@ public class EventIntegrationHandlerTests await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); + expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); @@ -304,7 +304,7 @@ public class EventIntegrationHandlerTests await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); - expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); + expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index b4a384d798..aa93567538 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -14,7 +14,7 @@ public class IntegrationHandlerTests var sut = new TestIntegrationHandler(); var expected = new IntegrationMessage() { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), + Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"), MessageId = "TestMessageId", IntegrationType = IntegrationType.Webhook, RenderedTemplate = "Template", diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 9a03fb28f0..bf4283243c 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -19,7 +19,7 @@ public class WebhookIntegrationHandlerTests private readonly HttpClient _httpClient; private const string _scheme = "Bearer"; private const string _token = "AUTH_TOKEN"; - private const string _webhookUrl = "http://localhost/test/event"; + private static readonly Uri _webhookUri = new Uri("https://localhost"); public WebhookIntegrationHandlerTests() { @@ -45,7 +45,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri); var result = await sutProvider.Sut.HandleAsync(message); @@ -63,7 +63,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(HttpMethod.Post, request.Method); Assert.Null(request.Headers.Authorization); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + Assert.Equal(_webhookUri, request.RequestUri); AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); } @@ -71,7 +71,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token); var result = await sutProvider.Sut.HandleAsync(message); @@ -89,7 +89,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + Assert.Equal(_webhookUri, request.RequestUri); AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); } @@ -101,7 +101,7 @@ public class WebhookIntegrationHandlerTests var retryAfter = now.AddSeconds(60); sutProvider.GetDependency().SetUtcNow(now); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -124,7 +124,7 @@ public class WebhookIntegrationHandlerTests var sutProvider = GetSutProvider(); var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc); var retryAfter = now.AddSeconds(60); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -145,7 +145,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.InternalServerError) @@ -164,7 +164,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TemporaryRedirect) diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index 155eceeb25..105b65d0da 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Text.Json; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Models.Data; using Bit.Test.Common.AutoFixture.Attributes; @@ -39,6 +40,16 @@ public class IntegrationTemplateProcessorTests Assert.Equal(expected, result); } + [Theory, BitAutoData] + public void ReplaceTokens_WithEventMessageToken_ReplacesWithSerializedJson(EventMessage eventMessage) + { + var template = "#EventMessage#"; + var expected = $"{JsonSerializer.Serialize(eventMessage)}"; + var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage); + + Assert.Equal(expected, result); + } + [Theory, BitAutoData] public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage) { From 8d547dcc280babab70dd4a3c94ced6a34b12dfbf Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:31:22 -0400 Subject: [PATCH 012/326] feat(DuckDuckGo): Added DuckDuckGo browser device type --- src/Core/Enums/DeviceType.cs | 4 +++- src/Core/Utilities/DeviceTypes.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/Enums/DeviceType.cs b/src/Core/Enums/DeviceType.cs index 9679088509..9f55f50bc0 100644 --- a/src/Core/Enums/DeviceType.cs +++ b/src/Core/Enums/DeviceType.cs @@ -55,5 +55,7 @@ public enum DeviceType : byte [Display(Name = "MacOs CLI")] MacOsCLI = 24, [Display(Name = "Linux CLI")] - LinuxCLI = 25 + LinuxCLI = 25, + [Display(Name = "DuckDuckGo")] + DuckDuckGoBrowser = 26, } diff --git a/src/Core/Utilities/DeviceTypes.cs b/src/Core/Utilities/DeviceTypes.cs index f42d1d9a2b..57dbe29b3d 100644 --- a/src/Core/Utilities/DeviceTypes.cs +++ b/src/Core/Utilities/DeviceTypes.cs @@ -45,6 +45,7 @@ public static class DeviceTypes DeviceType.IEBrowser, DeviceType.SafariBrowser, DeviceType.VivaldiBrowser, + DeviceType.DuckDuckGoBrowser, DeviceType.UnknownBrowser ]; From c6d38d9db3a38b1e0af08a9c9704018d926d74f8 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:18:47 +0200 Subject: [PATCH 013/326] Remove unused ctor dependencies from OrgExportController (#6018) Co-authored-by: Daniel James Smith --- .../Tools/Controllers/OrganizationExportController.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index 10b2d87456..b1925dd3cf 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -1,13 +1,11 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Tools.Models.Response; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; -using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Vault.Queries; -using Bit.Core.Vault.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -17,30 +15,21 @@ namespace Bit.Api.Tools.Controllers; [Authorize("Application")] public class OrganizationExportController : Controller { - private readonly ICurrentContext _currentContext; private readonly IUserService _userService; - private readonly ICipherService _cipherService; private readonly GlobalSettings _globalSettings; - private readonly IFeatureService _featureService; private readonly IAuthorizationService _authorizationService; private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly ICollectionRepository _collectionRepository; public OrganizationExportController( - ICurrentContext currentContext, - ICipherService cipherService, IUserService userService, GlobalSettings globalSettings, - IFeatureService featureService, IAuthorizationService authorizationService, IOrganizationCiphersQuery organizationCiphersQuery, ICollectionRepository collectionRepository) { - _currentContext = currentContext; - _cipherService = cipherService; _userService = userService; _globalSettings = globalSettings; - _featureService = featureService; _authorizationService = authorizationService; _organizationCiphersQuery = organizationCiphersQuery; _collectionRepository = collectionRepository; From b7df8525afe20909c49d3c50f4ef1a56f8783292 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Wed, 2 Jul 2025 14:56:15 -0500 Subject: [PATCH 014/326] PM-23030 adding migration script (#6009) * PM-23030 adding migration script * PM-23030 fixing store procedure sql file * PM-23030 fixing syntax error * PM-23030 fixing migration * PM-23030 fixing sql script * PM-23030 fixing migration order * PM_23030 fixing migrations * PM-23030 fixing migration script validation error * PM-23030 fixing migration * PM-23030 trying to fix validation error * PM-23030 fixing migration script * PM-23030 updating sql scripts to change data type * PM-23030 adding report key to organization application * PM-23030 adding report key migration scripts * PM-23030 adding migration scripts * PM-23030 changing key column name --- .../Dirt/Entities/OrganizationApplication.cs | 1 + src/Core/Dirt/Entities/OrganizationReport.cs | 2 + .../OrganizationApplication_Create.sql | 13 +- .../OrganizationReport_Create.sql | 13 +- .../Dirt/Tables/OrganizationApplication.sql | 3 +- .../dbo/Dirt/Tables/OrganizationReport.sql | 3 +- .../2025-06-26_00_AlterOrganizationReport.sql | 44 + ...-07-01_00_AlterOrganizationApplication.sql | 44 + ...00_AlterOrganizationReport.sql.Designer.cs | 3263 ++++++++++++++++ ...25-06-26_00_AlterOrganizationReport.sql.cs | 39 + ...terOrganizationApplication.sql.Designer.cs | 3263 ++++++++++++++++ ...-01_00_AlterOrganizationApplication.sql.cs | 21 + .../DatabaseContextModelSnapshot.cs | 8 + ...00_AlterOrganizationReport.sql.Designer.cs | 3269 +++++++++++++++++ ...25-06-26_00_AlterOrganizationReport.sql.cs | 39 + ...terOrganizationApplication.sql.Designer.cs | 3269 +++++++++++++++++ ...-01_00_AlterOrganizationApplication.sql.cs | 21 + .../DatabaseContextModelSnapshot.cs | 8 + ...00_AlterOrganizationReport.sql.Designer.cs | 3252 ++++++++++++++++ ...25-06-26_00_AlterOrganizationReport.sql.cs | 39 + ...terOrganizationApplication.sql.Designer.cs | 3252 ++++++++++++++++ ...-01_00_AlterOrganizationApplication.sql.cs | 21 + .../DatabaseContextModelSnapshot.cs | 8 + 23 files changed, 19883 insertions(+), 12 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-06-26_00_AlterOrganizationReport.sql create mode 100644 util/Migrator/DbScripts/2025-07-01_00_AlterOrganizationApplication.sql create mode 100644 util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.cs create mode 100644 util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.cs create mode 100644 util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.cs create mode 100644 util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.cs create mode 100644 util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.cs create mode 100644 util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.cs diff --git a/src/Core/Dirt/Entities/OrganizationApplication.cs b/src/Core/Dirt/Entities/OrganizationApplication.cs index 259dbd60dd..48a6ef4257 100644 --- a/src/Core/Dirt/Entities/OrganizationApplication.cs +++ b/src/Core/Dirt/Entities/OrganizationApplication.cs @@ -12,6 +12,7 @@ public class OrganizationApplication : ITableObject, IRevisable public string Applications { get; set; } = string.Empty; public DateTime CreationDate { get; set; } = DateTime.UtcNow; public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + public string ContentEncryptionKey { get; set; } = string.Empty; public void SetNewId() { diff --git a/src/Core/Dirt/Entities/OrganizationReport.cs b/src/Core/Dirt/Entities/OrganizationReport.cs index 0f327c5c8f..92975ca441 100644 --- a/src/Core/Dirt/Entities/OrganizationReport.cs +++ b/src/Core/Dirt/Entities/OrganizationReport.cs @@ -13,6 +13,8 @@ public class OrganizationReport : ITableObject public string ReportData { get; set; } = string.Empty; public DateTime CreationDate { get; set; } = DateTime.UtcNow; + public string ContentEncryptionKey { get; set; } = string.Empty; + public void SetNewId() { Id = CoreHelpers.GenerateComb(); diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql index b2bb8593ef..eff67e696b 100644 --- a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationApplication_Create.sql @@ -3,7 +3,8 @@ CREATE PROCEDURE [dbo].[OrganizationApplication_Create] @OrganizationId UNIQUEIDENTIFIER, @Applications NVARCHAR(MAX), @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7) + @RevisionDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX) AS SET NOCOUNT ON; @@ -13,13 +14,15 @@ AS [OrganizationId], [Applications], [CreationDate], - [RevisionDate] + [RevisionDate], + [ContentEncryptionKey] ) VALUES - ( + ( @Id, @OrganizationId, @Applications, @CreationDate, - @RevisionDate - ); + @RevisionDate, + @ContentEncryptionKey + ); diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql index d0cea4d73b..087d4b1e09 100644 --- a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql @@ -3,21 +3,24 @@ CREATE PROCEDURE [dbo].[OrganizationReport_Create] @OrganizationId UNIQUEIDENTIFIER, @Date DATETIME2(7), @ReportData NVARCHAR(MAX), - @CreationDate DATETIME2(7) + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX) AS SET NOCOUNT ON; - INSERT INTO [dbo].[OrganizationReport]( + INSERT INTO [dbo].[OrganizationReport]( [Id], [OrganizationId], [Date], [ReportData], - [CreationDate] + [CreationDate], + [ContentEncryptionKey] ) - VALUES ( + VALUES ( @Id, @OrganizationId, @Date, @ReportData, - @CreationDate + @CreationDate, + @ContentEncryptionKey ); diff --git a/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql b/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql index 58c8080e23..76cb8356a0 100644 --- a/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql +++ b/src/Sql/dbo/Dirt/Tables/OrganizationApplication.sql @@ -4,9 +4,10 @@ CREATE TABLE [dbo].[OrganizationApplication] ( [Applications] NVARCHAR(MAX) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [RevisionDate] DATETIME2 (7) NOT NULL, + [ContentEncryptionKey] VARCHAR(MAX) NOT NULL, CONSTRAINT [PK_OrganizationApplication] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_OrganizationApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) - ); +); GO CREATE NONCLUSTERED INDEX [IX_OrganizationApplication_OrganizationId] diff --git a/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql index 563877a340..edc7ff4c92 100644 --- a/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql +++ b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql @@ -4,9 +4,10 @@ CREATE TABLE [dbo].[OrganizationReport] ( [Date] DATETIME2 (7) NOT NULL, [ReportData] NVARCHAR(MAX) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, + [ContentEncryptionKey] VARCHAR(MAX) NOT NULL, CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) - ); +); GO CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId] diff --git a/util/Migrator/DbScripts/2025-06-26_00_AlterOrganizationReport.sql b/util/Migrator/DbScripts/2025-06-26_00_AlterOrganizationReport.sql new file mode 100644 index 0000000000..5332a7f38b --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-26_00_AlterOrganizationReport.sql @@ -0,0 +1,44 @@ +IF COL_LENGTH('[dbo].[OrganizationReport]', 'ContentEncryptionKey') IS NULL +BEGIN + ALTER TABLE [dbo].[OrganizationReport] + ADD [ContentEncryptionKey] VARCHAR(MAX) NOT NULL; +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationReportView] +AS +SELECT + * +FROM + [dbo].[OrganizationReport] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Date DATETIME2(7), + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationReport] + ( + [Id], + [OrganizationId], + [Date], + [ReportData], + [CreationDate], + [ContentEncryptionKey] + ) + VALUES + ( + @Id, + @OrganizationId, + @Date, + @ReportData, + @CreationDate, + @ContentEncryptionKey + ); +GO diff --git a/util/Migrator/DbScripts/2025-07-01_00_AlterOrganizationApplication.sql b/util/Migrator/DbScripts/2025-07-01_00_AlterOrganizationApplication.sql new file mode 100644 index 0000000000..780e152be1 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-01_00_AlterOrganizationApplication.sql @@ -0,0 +1,44 @@ +IF COL_LENGTH('[dbo].[OrganizationApplication]', 'ContentEncryptionKey') IS NULL +BEGIN +ALTER TABLE [dbo].[OrganizationApplication] + ADD [ContentEncryptionKey] VARCHAR(MAX) NOT NULL; +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationApplicationView] +AS +SELECT + * +FROM + [dbo].[OrganizationApplication] +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationApplication_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Applications NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX) +AS + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationApplication] + ( + [Id], + [OrganizationId], + [Applications], + [CreationDate], + [RevisionDate], + [ContentEncryptionKey] + ) + VALUES + ( + @Id, + @OrganizationId, + @Applications, + @CreationDate, + @RevisionDate, + @ContentEncryptionKey + ); +GO diff --git a/util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..fe766b766c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs @@ -0,0 +1,3263 @@ +// +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("20250702155123_2025-06-26_00_AlterOrganizationReport.sql")] + partial class _20250626_00_AlterOrganizationReportsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.cs b/util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.cs new file mode 100644 index 0000000000..c08a994cf4 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250702155123_2025-06-26_00_AlterOrganizationReport.sql.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250626_00_AlterOrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication"); + } +} diff --git a/util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs b/util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs new file mode 100644 index 0000000000..e919aa5e5c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs @@ -0,0 +1,3263 @@ +// +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("20250702155151_2025-07-01_00_AlterOrganizationApplication.sql")] + partial class _20250701_00_AlterOrganizationApplicationsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.cs b/util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.cs new file mode 100644 index 0000000000..414c266c35 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250702155151_2025-07-01_00_AlterOrganizationApplication.sql.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250701_00_AlterOrganizationApplicationsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 0f983f8f06..fbe40a8d2b 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -979,6 +979,10 @@ namespace Bit.MySqlMigrations.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + b.Property("CreationDate") .HasColumnType("datetime(6)"); @@ -1004,6 +1008,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Id") .HasColumnType("char(36)"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + b.Property("CreationDate") .HasColumnType("datetime(6)"); diff --git a/util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs b/util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..5744bd5fb0 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs @@ -0,0 +1,3269 @@ +// +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("20250702155127_2025-06-26_00_AlterOrganizationReport.sql")] + partial class _20250626_00_AlterOrganizationReportsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.cs b/util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.cs new file mode 100644 index 0000000000..0167c4e24a --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250702155127_2025-06-26_00_AlterOrganizationReport.sql.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250626_00_AlterOrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication"); + } +} diff --git a/util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs b/util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs new file mode 100644 index 0000000000..344898da1b --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs @@ -0,0 +1,3269 @@ +// +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("20250702155155_2025-07-01_00_AlterOrganizationApplication.sql")] + partial class _20250701_00_AlterOrganizationApplicationsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.cs b/util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.cs new file mode 100644 index 0000000000..87fcc3a20d --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250702155155_2025-07-01_00_AlterOrganizationApplication.sql.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250701_00_AlterOrganizationApplicationsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index aaf6124d7b..8a99b3aa9a 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -984,6 +984,10 @@ namespace Bit.PostgresMigrations.Migrations .IsRequired() .HasColumnType("text"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -1009,6 +1013,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Id") .HasColumnType("uuid"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); diff --git a/util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs b/util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs new file mode 100644 index 0000000000..b0c1a23ae0 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.Designer.cs @@ -0,0 +1,3252 @@ +// +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("20250702155118_2025-06-26_00_AlterOrganizationReport.sql")] + partial class _20250626_00_AlterOrganizationReportsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.cs b/util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.cs new file mode 100644 index 0000000000..a46d397541 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250702155118_2025-06-26_00_AlterOrganizationReport.sql.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250626_00_AlterOrganizationReportsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "ContentEncryptionKey", + table: "OrganizationApplication"); + } +} diff --git a/util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs b/util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs new file mode 100644 index 0000000000..be3ee69abd --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.Designer.cs @@ -0,0 +1,3252 @@ +// +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("20250702155159_2025-07-01_00_AlterOrganizationApplication.sql")] + partial class _20250701_00_AlterOrganizationApplicationsql + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.cs b/util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.cs new file mode 100644 index 0000000000..4bc6ee9f45 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250702155159_2025-07-01_00_AlterOrganizationApplication.sql.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250701_00_AlterOrganizationApplicationsql : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 4bed680478..d10e5df4cf 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -968,6 +968,10 @@ namespace Bit.SqliteMigrations.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("CreationDate") .HasColumnType("TEXT"); @@ -993,6 +997,10 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Id") .HasColumnType("TEXT"); + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("CreationDate") .HasColumnType("TEXT"); From 669a5cb372478a31872af00a9ce1c7990b1136d4 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:28:48 -0400 Subject: [PATCH 015/326] [SM-1273] Adding new logging for secrets (#5991) * Adding new logging for secrets * fixing secrest controller tests * fixing the tests --- .../Controllers/SecretsController.cs | 32 ++++++++++++------- src/Core/AdminConsole/Enums/EventType.cs | 3 ++ .../AdminConsole/Services/IEventService.cs | 2 +- .../Services/Implementations/EventService.cs | 25 +++++++++++++-- .../NoopImplementations/NoopEventService.cs | 2 +- .../Controllers/SecretsControllerTests.cs | 30 ++++++++++++++--- 6 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index dd653bb873..519bc328fa 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -109,7 +109,7 @@ public class SecretsController : Controller } var result = await _createSecretCommand.CreateAsync(secret, accessPoliciesUpdates); - + await LogSecretEventAsync(secret, EventType.Secret_Created); // Creating a secret means you have read & write permission. return new SecretResponseModel(result, true, true); } @@ -135,10 +135,7 @@ public class SecretsController : Controller throw new NotFoundException(); } - if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) - { - await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved); - } + await LogSecretEventAsync(secret, EventType.Secret_Retrieved); return new SecretResponseModel(secret, access.Read, access.Write); } @@ -188,10 +185,10 @@ public class SecretsController : Controller { throw new NotFoundException(); } - } var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates); + await LogSecretEventAsync(secret, EventType.Secret_Edited); // Updating a secret means you have read & write permission. return new SecretResponseModel(result, true, true); @@ -234,6 +231,7 @@ public class SecretsController : Controller await _deleteSecretCommand.DeleteSecrets(secretsToDelete); var responses = results.Select(r => new BulkDeleteResponseModel(r.Secret.Id, r.Error)); + await LogSecretsEventAsync(secretsToDelete, EventType.Secret_Deleted); return new ListResponseModel(responses); } @@ -253,7 +251,7 @@ public class SecretsController : Controller throw new NotFoundException(); } - await LogSecretsRetrievalAsync(secrets); + await LogSecretsEventAsync(secrets, EventType.Secret_Retrieved); var responses = secrets.Select(s => new BaseSecretResponseModel(s)); return new ListResponseModel(responses); @@ -290,18 +288,28 @@ public class SecretsController : Controller if (syncResult.HasChanges) { - await LogSecretsRetrievalAsync(syncResult.Secrets); + await LogSecretsEventAsync(syncResult.Secrets, EventType.Secret_Retrieved); } return new SecretsSyncResponseModel(syncResult.HasChanges, syncResult.Secrets); } - private async Task LogSecretsRetrievalAsync(IEnumerable secrets) + private async Task LogSecretsEventAsync(IEnumerable secrets, EventType eventType) { - if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) + var userId = _userService.GetProperUserId(User)!.Value; + + switch (_currentContext.IdentityClientType) { - var userId = _userService.GetProperUserId(User)!.Value; - await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, EventType.Secret_Retrieved); + case IdentityClientType.ServiceAccount: + await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType); + break; + case IdentityClientType.User: + await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType); + break; } } + + private Task LogSecretEventAsync(Secret secret, EventType eventType) => + LogSecretsEventAsync(new[] { secret }, eventType); + } diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/AdminConsole/Enums/EventType.cs index 9d9cb09989..2359b922d8 100644 --- a/src/Core/AdminConsole/Enums/EventType.cs +++ b/src/Core/AdminConsole/Enums/EventType.cs @@ -90,4 +90,7 @@ public enum EventType : int OrganizationDomain_NotVerified = 2003, Secret_Retrieved = 2100, + Secret_Created = 2101, + Secret_Edited = 2102, + Secret_Deleted = 2103, } diff --git a/src/Core/AdminConsole/Services/IEventService.cs b/src/Core/AdminConsole/Services/IEventService.cs index 5b4f8731a2..14ef4ba4d4 100644 --- a/src/Core/AdminConsole/Services/IEventService.cs +++ b/src/Core/AdminConsole/Services/IEventService.cs @@ -30,6 +30,6 @@ public interface IEventService Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events); Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null); Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null); - Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null); + Task LogUserSecretsEventAsync(Guid userId, IEnumerable secrets, EventType type, DateTime? date = null); Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable secrets, EventType type, DateTime? date = null); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index 88d9595b4a..d21e6f25e8 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -409,9 +409,30 @@ public class EventService : IEventService await _eventWriteService.CreateAsync(e); } - public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null) + public async Task LogUserSecretsEventAsync(Guid userId, IEnumerable secrets, EventType type, DateTime? date = null) { - await LogServiceAccountSecretsEventAsync(serviceAccountId, new[] { secret }, type, date); + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + + foreach (var secret in secrets) + { + if (!CanUseEvents(orgAbilities, secret.OrganizationId)) + { + continue; + } + + var e = new EventMessage(_currentContext) + { + OrganizationId = secret.OrganizationId, + Type = type, + SecretId = secret.Id, + UserId = userId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + } + + await _eventWriteService.CreateManyAsync(eventMessages); } public async Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable secrets, EventType type, DateTime? date = null) diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs index d96c4a0ce1..b1ff5b1c4a 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs @@ -116,7 +116,7 @@ public class NoopEventService : IEventService return Task.FromResult(0); } - public Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, + public Task LogUserSecretsEventAsync(Guid userId, IEnumerable secrets, EventType type, DateTime? date = null) { return Task.FromResult(0); diff --git a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs index 4fb2c4b7fb..83a4229f39 100644 --- a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs @@ -19,6 +19,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using NSubstitute; using Xunit; @@ -152,6 +153,7 @@ public class SecretsControllerTests SecretCreateRequestModel data, Guid organizationId) { data = SetupSecretCreateRequest(sutProvider, data, organizationId); + SetControllerUser(sutProvider, new Guid()); await sutProvider.Sut.CreateAsync(organizationId, data); @@ -186,6 +188,7 @@ public class SecretsControllerTests .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).Returns(AuthorizationResult.Success()); + SetControllerUser(sutProvider, new Guid()); await sutProvider.Sut.CreateAsync(organizationId, data); @@ -199,6 +202,7 @@ public class SecretsControllerTests SecretUpdateRequestModel data, Secret currentSecret) { data = SetupSecretUpdateRequest(data); + sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); @@ -239,7 +243,7 @@ public class SecretsControllerTests SecretUpdateRequestModel data, Secret currentSecret) { data = SetupSecretUpdateRequest(data); - + SetControllerUser(sutProvider, new Guid()); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); @@ -260,7 +264,6 @@ public class SecretsControllerTests SecretUpdateRequestModel data, Secret currentSecret, SecretAccessPoliciesUpdates accessPoliciesUpdates) { data = SetupSecretUpdateAccessPoliciesRequest(sutProvider, data, currentSecret, accessPoliciesUpdates); - sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).Returns(AuthorizationResult.Failed()); @@ -339,6 +342,8 @@ public class SecretsControllerTests { var ids = data.Select(s => s.Id).ToList(); var organizationId = data.First().OrganizationId; + SetControllerUser(sutProvider, new Guid()); + foreach (var secret in data) { secret.OrganizationId = organizationId; @@ -378,7 +383,7 @@ public class SecretsControllerTests sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); - + SetControllerUser(sutProvider, new Guid()); var results = await sutProvider.Sut.BulkDeleteAsync(ids); await sutProvider.GetDependency().Received(1) @@ -434,7 +439,7 @@ public class SecretsControllerTests { var (ids, request) = BuildGetSecretsRequestModel(data); var organizationId = SetOrganizations(ref data); - + SetControllerUser(sutProvider, new Guid()); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data, @@ -507,7 +512,7 @@ public class SecretsControllerTests SutProvider sutProvider, Guid organizationId) { var lastSyncedDate = SetupSecretsSyncRequest(nullLastSyncedDate, secrets, sutProvider, organizationId); - + SetControllerUser(sutProvider, new Guid()); var result = await sutProvider.Sut.GetSecretsSyncAsync(organizationId, lastSyncedDate); Assert.True(result.HasChanges); Assert.NotNull(result.Secrets); @@ -610,4 +615,19 @@ public class SecretsControllerTests .ReturnsForAnyArgs(data.ToSecret(currentSecret)); return data; } + + private static void SetControllerUser(SutProvider sutProvider, Guid userId) + { + var claims = new List { new Claim(ClaimTypes.NameIdentifier, userId.ToString()) }; + var identity = new ClaimsIdentity(claims, "Test"); + var principal = new ClaimsPrincipal(identity); + + sutProvider.Sut.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + }; + + sutProvider.GetDependency().GetProperUserId(principal).Returns(userId); + } + } From fafdfd6fbda5ad68a6026786cafcccb4aa9c71b4 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:40:34 -0400 Subject: [PATCH 016/326] Migrate AC code to have `#nullable disable` (#6027) --- src/Core/AdminConsole/Context/CurrentContextProvider.cs | 5 ++++- src/Core/AdminConsole/Models/Business/ImportedGroup.cs | 5 ++++- .../AdminConsole/Models/Business/ImportedOrganizationUser.cs | 5 ++++- src/Core/AdminConsole/Models/Business/InviteOrganization.cs | 5 ++++- .../AdminConsole/Models/Business/OrganizationUserInvite.cs | 5 ++++- .../Models/Business/Provider/ProviderUserInvite.cs | 5 ++++- src/Core/AdminConsole/Models/Data/EventMessage.cs | 5 ++++- src/Core/AdminConsole/Models/Data/EventTableEntity.cs | 5 ++++- src/Core/AdminConsole/Models/Data/GroupWithCollections.cs | 5 ++++- .../OrganizationUsers/OrganizationUserInviteData.cs | 5 ++++- .../OrganizationUsers/OrganizationUserOrganizationDetails.cs | 5 ++++- .../OrganizationUsers/OrganizationUserPolicyDetails.cs | 5 ++++- .../OrganizationUsers/OrganizationUserPublicKey.cs | 5 ++++- .../OrganizationUserResetPasswordDetails.cs | 5 ++++- .../OrganizationUsers/OrganizationUserUserDetails.cs | 5 ++++- .../OrganizationUsers/OrganizationUserWithCollections.cs | 5 ++++- .../Data/Organizations/SelfHostedOrganizationDetails.cs | 5 ++++- .../Data/Provider/ProviderOrganizationOrganizationDetails.cs | 5 ++++- .../Data/Provider/ProviderOrganizationProviderDetails.cs | 5 ++++- .../Models/Data/Provider/ProviderUserOrganizationDetails.cs | 5 ++++- .../Models/Data/Provider/ProviderUserProviderDetails.cs | 5 ++++- .../Models/Data/Provider/ProviderUserPublicKey.cs | 5 ++++- .../Models/Data/Provider/ProviderUserUserDetails.cs | 5 ++++- .../Models/Mail/DeviceApprovalRequestedViewModel.cs | 5 ++++- .../OrganizationAuth/Models/AuthRequestUpdateProcessor.cs | 5 ++++- .../Models/BatchAuthRequestUpdateProcessor.cs | 5 ++++- .../OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs | 5 ++++- .../OrganizationFeatures/Groups/CreateGroupCommand.cs | 5 ++++- .../Groups/Interfaces/ICreateGroupCommand.cs | 5 ++++- .../Groups/Interfaces/IUpdateGroupCommand.cs | 5 ++++- .../OrganizationApiKeys/GetOrganizationApiKeyQuery.cs | 5 ++++- .../OrganizationConnections/ValidateBillingSyncKeyCommand.cs | 5 ++++- .../GetOrganizationDomainByIdOrganizationIdQuery.cs | 5 ++++- .../OrganizationDomains/VerifyOrganizationDomainCommand.cs | 5 ++++- .../OrganizationUsers/AcceptOrgUserCommand.cs | 5 ++++- .../OrganizationUserUserMiniDetailsAuthorizationHandler.cs | 5 ++++- .../OrganizationUsers/ConfirmOrganizationUserCommand.cs | 5 ++++- .../Interfaces/IConfirmOrganizationUserCommand.cs | 5 ++++- .../InviteUsers/InviteOrganizationUsersCommand.cs | 5 ++++- .../InviteUsers/Models/CreateOrganizationUser.cs | 5 ++++- .../InviteUsers/Models/InviteOrganizationUsersResponse.cs | 5 ++++- .../Models/InviteOrganizationUsersValidationRequest.cs | 5 ++++- .../InviteUsers/SendOrganizationInvitesCommand.cs | 5 ++++- .../Validation/InviteOrganizationUserValidator.cs | 5 ++++- .../PasswordManager/InviteUsersPasswordManagerValidator.cs | 5 ++++- .../InviteUsers/Validation/Payments/PaymentsSubscription.cs | 5 ++++- .../OrganizationUsers/RemoveOrganizationUserCommand.cs | 5 ++++- .../RestoreUser/v1/RestoreOrganizationUserCommand.cs | 5 ++++- .../Organizations/CloudOrganizationSignUpCommand.cs | 5 ++++- .../Organizations/InitPendingOrganizationCommand.cs | 5 ++++- .../Organizations/OrganizationUpdateKeysCommand.cs | 5 ++++- .../Organizations/ProviderClientOrganizationSignUpCommand.cs | 5 ++++- .../PolicyRequirements/ResetPasswordPolicyRequirement.cs | 5 ++++- src/Core/AdminConsole/Services/IIntegrationHandler.cs | 5 ++++- src/Core/AdminConsole/Services/IProviderService.cs | 5 ++++- .../AdminConsole/Services/Implementations/EventService.cs | 5 ++++- .../Services/Implementations/OrganizationService.cs | 5 ++++- .../AdminConsole/Services/Implementations/PolicyService.cs | 5 ++++- .../Services/NoopImplementations/NoopProviderService.cs | 5 ++++- 59 files changed, 236 insertions(+), 59 deletions(-) diff --git a/src/Core/AdminConsole/Context/CurrentContextProvider.cs b/src/Core/AdminConsole/Context/CurrentContextProvider.cs index 78a5565e80..5be25171d0 100644 --- a/src/Core/AdminConsole/Context/CurrentContextProvider.cs +++ b/src/Core/AdminConsole/Context/CurrentContextProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Models.Data; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Business/ImportedGroup.cs b/src/Core/AdminConsole/Models/Business/ImportedGroup.cs index bd4e81bf5b..7e4cddb496 100644 --- a/src/Core/AdminConsole/Models/Business/ImportedGroup.cs +++ b/src/Core/AdminConsole/Models/Business/ImportedGroup.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; namespace Bit.Core.AdminConsole.Models.Business; diff --git a/src/Core/AdminConsole/Models/Business/ImportedOrganizationUser.cs b/src/Core/AdminConsole/Models/Business/ImportedOrganizationUser.cs index 967cdf253d..273d2ee3b3 100644 --- a/src/Core/AdminConsole/Models/Business/ImportedOrganizationUser.cs +++ b/src/Core/AdminConsole/Models/Business/ImportedOrganizationUser.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Business; public class ImportedOrganizationUser { diff --git a/src/Core/AdminConsole/Models/Business/InviteOrganization.cs b/src/Core/AdminConsole/Models/Business/InviteOrganization.cs index 175ee07a9f..56b3259bc4 100644 --- a/src/Core/AdminConsole/Models/Business/InviteOrganization.cs +++ b/src/Core/AdminConsole/Models/Business/InviteOrganization.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.StaticStore; namespace Bit.Core.AdminConsole.Models.Business; diff --git a/src/Core/AdminConsole/Models/Business/OrganizationUserInvite.cs b/src/Core/AdminConsole/Models/Business/OrganizationUserInvite.cs index e177a5047b..5b59102173 100644 --- a/src/Core/AdminConsole/Models/Business/OrganizationUserInvite.cs +++ b/src/Core/AdminConsole/Models/Business/OrganizationUserInvite.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/Models/Business/Provider/ProviderUserInvite.cs b/src/Core/AdminConsole/Models/Business/Provider/ProviderUserInvite.cs index 53cdefb3f9..061caffdd7 100644 --- a/src/Core/AdminConsole/Models/Business/Provider/ProviderUserInvite.cs +++ b/src/Core/AdminConsole/Models/Business/Provider/ProviderUserInvite.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums.Provider; namespace Bit.Core.AdminConsole.Models.Business.Provider; diff --git a/src/Core/AdminConsole/Models/Data/EventMessage.cs b/src/Core/AdminConsole/Models/Data/EventMessage.cs index 6d2a1f2b4e..7c2c29f80f 100644 --- a/src/Core/AdminConsole/Models/Data/EventMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventMessage.cs @@ -1,4 +1,7 @@ -using Bit.Core.Context; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Context; using Bit.Core.Enums; namespace Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs index 7e863b128c..410ad67f0e 100644 --- a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs +++ b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs @@ -1,4 +1,7 @@ -using Azure; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Azure; using Azure.Data.Tables; using Bit.Core.Enums; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/GroupWithCollections.cs b/src/Core/AdminConsole/Models/Data/GroupWithCollections.cs index e9ba512574..6ec47990ae 100644 --- a/src/Core/AdminConsole/Models/Data/GroupWithCollections.cs +++ b/src/Core/AdminConsole/Models/Data/GroupWithCollections.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.AdminConsole.Entities; namespace Bit.Core.AdminConsole.Models.Data; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs index a48ee3a6c4..7f1034f50e 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 8de422ee31..bad06ccf64 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs index 84939ecf79..0f5f5fd7c6 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPolicyDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.Enums; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs index 7c04967872..b51675bdc5 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; public class OrganizationUserPublicKey { diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs index 05d6807fad..f2ed0c0ba2 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs index 64ee316ab6..6d182e197f 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Interfaces; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs index d86c6c1581..02d83597e2 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Entities; namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index a6ad47f829..68458b09ec 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Auth.Entities; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs index 9d84f60c4c..0a6d255774 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationOrganizationDetails.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using System.Text.Json.Serialization; using Bit.Core.Billing.Enums; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationProviderDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationProviderDetails.cs index 629e0bae53..77ca501526 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationProviderDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderOrganizationProviderDetails.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index 4621de8268..04281d098e 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserProviderDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserProviderDetails.cs index 67565bad6d..2ab06bacae 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserProviderDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserProviderDetails.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserPublicKey.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserPublicKey.cs index a9b37b2050..18a0679702 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserPublicKey.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserPublicKey.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.AdminConsole.Models.Data.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.AdminConsole.Models.Data.Provider; public class ProviderUserPublicKey { diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserUserDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserUserDetails.cs index d42437a26e..97f72f7137 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserUserDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserUserDetails.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs b/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs index 7f6c932619..892d077296 100644 --- a/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs +++ b/src/Core/AdminConsole/Models/Mail/DeviceApprovalRequestedViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.AdminConsole.Models.Mail; diff --git a/src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessor.cs b/src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessor.cs index 59b5025eeb..da33289630 100644 --- a/src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessor.cs +++ b/src/Core/AdminConsole/OrganizationAuth/Models/AuthRequestUpdateProcessor.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Auth.Models.Data; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessor.cs b/src/Core/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessor.cs index 3ebcd1fb51..76d5c4b321 100644 --- a/src/Core/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessor.cs +++ b/src/Core/AdminConsole/OrganizationAuth/Models/BatchAuthRequestUpdateProcessor.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Data; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationAuth.Models; diff --git a/src/Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs b/src/Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs index 5a4b4ed763..c829ed0ad6 100644 --- a/src/Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs +++ b/src/Core/AdminConsole/OrganizationAuth/Models/OrganizationAuthRequestUpdate.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.AdminConsole.OrganizationAuth.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.AdminConsole.OrganizationAuth.Models; public class OrganizationAuthRequestUpdate { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs index f514beed38..86a222439e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/CreateGroupCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/ICreateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/ICreateGroupCommand.cs index b3ad06d9dc..13a5a00f43 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/ICreateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/ICreateGroupCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IUpdateGroupCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IUpdateGroupCommand.cs index 1cae4805f2..4ef95ceeae 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IUpdateGroupCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Groups/Interfaces/IUpdateGroupCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs index b91e57a67c..f4a3b96372 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs index 3e19c773ef..ccc56297df 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs index 12616a142a..5f9c102208 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/GetOrganizationDomainByIdOrganizationIdQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Entities; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 43a3120ffd..c03341bbc0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 3770d867cf..63f177b3f3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs index 01b77a05b3..e63b6bf096 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Microsoft.AspNetCore.Authorization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 62e5d60191..4ede530585 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index 734b8d2b0c..cf5999f892 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 1dddc8bf0c..addb1997a9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums.Provider; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Business; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUser.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUser.cs index a55db3958a..bde56a66e8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUser.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUser.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Data; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs index ac7d864dd4..5e461e7d0b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersResponse.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs index f45c705cab..56812e2617 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs index ba85ce1d8a..cd5066d11b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 54f26cb46a..837ac6f055 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Validation; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs index f5259d1066..67155fe91a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/PaymentsSubscription.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/PaymentsSubscription.cs index dea35c4ddd..fcde0f9ebf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/PaymentsSubscription.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/PaymentsSubscription.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index 00d3ebb533..d1eec1bc76 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs index 0d9955eecf..651a9225b4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index f26061cbd2..96fcc087e6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs index 3e060c66a5..6474914b48 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Entities; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs index aa85c7e2a4..3f26ca372c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateKeysCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index c3e945b65f..27e70fbe2d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Pricing; using Bit.Core.Context; using Bit.Core.Entities; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs index b7d0b14f15..1d703fa4d4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/ResetPasswordPolicyRequirement.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs index e02f26a873..9a3edac9ec 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IProviderService.cs b/src/Core/AdminConsole/Services/IProviderService.cs index e4b6f3aabd..66c49d90c6 100644 --- a/src/Core/AdminConsole/Services/IProviderService.cs +++ b/src/Core/AdminConsole/Services/IProviderService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.Billing.Models; using Bit.Core.Entities; diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index d21e6f25e8..e56b3aced4 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Interfaces; using Bit.Core.AdminConsole.Models.Data.Provider; diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index d648eef2c9..7035641b46 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// 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.Enums; using Bit.Core.AdminConsole.Enums.Provider; diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index d424bd8fff..5ba39e8054 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs index 94c1096b58..2bf4a54a87 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.Billing.Models; using Bit.Core.Entities; From 3302f052761229fc71921e651b1b9172f40d9d3e Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:40:55 -0400 Subject: [PATCH 017/326] Migrate KM code to have `#nullable disable` (#6023) --- .../KeyManagement/Models/Data/RotateUserAccountKeysData.cs | 5 ++++- .../KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs index f81baf6fab..b89f19797f 100644 --- a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// 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; using Bit.Core.Tools.Entities; diff --git a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs index 1550f3c186..c8bd7cab1f 100644 --- a/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.KeyManagement.Models.Data; using Microsoft.AspNetCore.Identity; using Microsoft.Data.SqlClient; From b17f0ca41c48fadb9262f198867a29115627085c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:14:55 +0200 Subject: [PATCH 018/326] [deps] Tools: Update MailKit to 4.13.0 (#6045) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a6be8f484e..0a110381f7 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,7 +34,7 @@ - + From 5dde9ac924efc4018ae50a0cf9f91ea167d75333 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:16:53 +0200 Subject: [PATCH 019/326] [deps] Tools: Update aws-sdk-net monorepo (#6039) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 0a110381f7..85251b3185 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 240968ef4cfa635bf26c0e5abcb638c141043410 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 7 Jul 2025 09:24:57 -0500 Subject: [PATCH 020/326] Refactor `PendingSecurityTasks` to `RefreshSecurityTasks` (#5903) - Allows for more general use case of security task notifications --- src/Core/Enums/PushType.cs | 2 +- .../Platform/Push/Services/IPushNotificationService.cs | 4 ++-- .../Vault/Commands/CreateManyTaskNotificationsCommand.cs | 2 +- .../Commands/MarkNotificationsForTaskAsDeletedCommand.cs | 2 +- src/Notifications/HubHelpers.cs | 2 +- .../Platform/Controllers/PushControllerTests.cs | 2 +- .../Services/AzureQueuePushNotificationServiceTests.cs | 4 ++-- .../NotificationsApiPushNotificationServiceTests.cs | 2 +- test/Core.Test/Platform/Push/Services/PushTestBase.cs | 8 ++++---- .../Push/Services/RelayPushNotificationServiceTests.cs | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs index 96a1192478..07c40f94a2 100644 --- a/src/Core/Enums/PushType.cs +++ b/src/Core/Enums/PushType.cs @@ -31,5 +31,5 @@ public enum PushType : byte Notification = 20, NotificationStatus = 21, - PendingSecurityTasks = 22 + RefreshSecurityTasks = 22 } diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/Services/IPushNotificationService.cs index 58b8a4722d..c6da6cf6b7 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/IPushNotificationService.cs @@ -389,10 +389,10 @@ public interface IPushNotificationService ExcludeCurrentContext = false, }); - Task PushPendingSecurityTasksAsync(Guid userId) + Task PushRefreshSecurityTasksAsync(Guid userId) => PushAsync(new PushNotification { - Type = PushType.PendingSecurityTasks, + Type = PushType.RefreshSecurityTasks, Target = NotificationTarget.User, TargetId = userId, Payload = new UserPushNotification diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs index e68a2ed726..98a4bea591 100644 --- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs +++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs @@ -89,7 +89,7 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo } // Notify the user that they have pending security tasks - await _pushNotificationService.PushPendingSecurityTasksAsync(userId); + await _pushNotificationService.PushRefreshSecurityTasksAsync(userId); } } } diff --git a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs index 8d1e6e4538..65fe98a4d2 100644 --- a/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs +++ b/src/Core/Vault/Commands/MarkNotificationsForTaskAsDeletedCommand.cs @@ -26,7 +26,7 @@ public class MarkNotificationsForTaskAsDeletedCommand : IMarkNotificationsForTas var uniqueUserIds = userIds.Distinct(); foreach (var id in uniqueUserIds) { - await _pushNotificationService.PushPendingSecurityTasksAsync(id); + await _pushNotificationService.PushRefreshSecurityTasksAsync(id); } } } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 441842da3b..c8ce3ecfe5 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -135,7 +135,7 @@ public static class HubHelpers } break; - case PushType.PendingSecurityTasks: + case PushType.RefreshSecurityTasks: var pendingTasksData = JsonSerializer.Deserialize>(notificationJson, _deserializerOptions); await hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString()) .SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken); diff --git a/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs index 4d86817a11..32d6389616 100644 --- a/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs +++ b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs @@ -166,7 +166,7 @@ public class PushControllerTests yield return UserTyped(PushType.SyncOrgKeys); yield return UserTyped(PushType.SyncSettings); yield return UserTyped(PushType.LogOut); - yield return UserTyped(PushType.PendingSecurityTasks); + yield return UserTyped(PushType.RefreshSecurityTasks); yield return Typed(new PushSendRequestModel { diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index 4e2ec19086..b223ef7252 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -707,7 +707,7 @@ public class AzureQueuePushNotificationServiceTests } [Fact] - public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + public async Task PushRefreshSecurityTasksAsync_SendsExpectedResponse() { var userId = Guid.NewGuid(); @@ -722,7 +722,7 @@ public class AzureQueuePushNotificationServiceTests }; await VerifyNotificationAsync( - async sut => await sut.PushPendingSecurityTasksAsync(userId), + async sut => await sut.PushRefreshSecurityTasksAsync(userId), expectedPayload ); } diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index 92706c6ccc..5231456d63 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -368,7 +368,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase }; } - protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) + protected override JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId) { return new JsonObject { diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs index 3538a68127..3ff09f1064 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -93,7 +93,7 @@ public abstract class PushTestBase protected abstract JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId); protected abstract JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization); protected abstract JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization); - protected abstract JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId); + protected abstract JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId); [Fact] public async Task PushSyncCipherCreateAsync_SendsExpectedResponse() @@ -444,13 +444,13 @@ public abstract class PushTestBase } [Fact] - public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + public async Task PushRefreshSecurityTasksAsync_SendsExpectedResponse() { var userId = Guid.NewGuid(); await VerifyNotificationAsync( - async sut => await sut.PushPendingSecurityTasksAsync(userId), - GetPushPendingSecurityTasksResponsePayload(userId) + async sut => await sut.PushRefreshSecurityTasksAsync(userId), + GetPushRefreshSecurityTasksResponsePayload(userId) ); } diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index f95531c944..ddad05eda0 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -491,7 +491,7 @@ public class RelayPushNotificationServiceTests : PushTestBase }; } - protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) + protected override JsonNode GetPushRefreshSecurityTasksResponsePayload(Guid userId) { return new JsonObject { From 79ad1dbda014709de1d85e92903cb4e2e092259d Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:56:59 -0400 Subject: [PATCH 021/326] fix(2fa): [PM-22323] Do not show 2FA warning for 2FA setup and login emails * Added configuration to not display 2FA setup instruction * Refactored to new service. * Linting. * Dependency injection * Changed to scoped to have access to ICurrentContext. * Inverted logic for EmailTotpAction * Fixed tests. * Fixed tests. * More tests. * Fixed tests. * Linting. * Added tests at controller level. * Linting * Fixed error in test. * Review updates. * Accidentally deleted imports. --- .../Auth/Controllers/AccountsController.cs | 17 +- .../Auth/Controllers/TwoFactorController.cs | 16 +- src/Core/Auth/Enums/TwoFactorEmailPurpose.cs | 8 + .../Auth/Services/ITwoFactorEmailService.cs | 11 + .../Implementations/TwoFactorEmailService.cs | 116 ++++++++ .../Handlebars/Auth/TwoFactorEmail.html.hbs | 4 +- .../Mail/TwoFactorEmailTokenViewModel.cs | 5 + src/Core/Services/IMailService.cs | 3 +- src/Core/Services/IUserService.cs | 16 -- .../Implementations/HandlebarsMailService.cs | 8 +- .../Services/Implementations/UserService.cs | 64 +---- .../NoopImplementations/NoopMailService.cs | 3 +- .../RequestValidators/DeviceValidator.cs | 5 +- .../Utilities/ServiceCollectionExtensions.cs | 1 + .../Controllers/AccountsControllerTests.cs | 48 +++- .../Services/TwoFactorEmailServiceTests.cs | 254 ++++++++++++++++++ test/Core.Test/Services/UserServiceTests.cs | 192 ------------- .../IdentityServer/DeviceValidatorTests.cs | 8 +- 18 files changed, 491 insertions(+), 288 deletions(-) create mode 100644 src/Core/Auth/Enums/TwoFactorEmailPurpose.cs create mode 100644 src/Core/Auth/Services/ITwoFactorEmailService.cs create mode 100644 src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs create mode 100644 test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index ec542daec7..695042bda7 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; @@ -34,6 +35,8 @@ public class AccountsController : Controller private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; + private readonly ITwoFactorEmailService _twoFactorEmailService; + public AccountsController( IOrganizationService organizationService, @@ -44,7 +47,8 @@ public class AccountsController : Controller ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService + IFeatureService featureService, + ITwoFactorEmailService twoFactorEmailService ) { _organizationService = organizationService; @@ -56,6 +60,8 @@ public class AccountsController : Controller _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; + _twoFactorEmailService = twoFactorEmailService; + } @@ -619,7 +625,14 @@ public class AccountsController : Controller [HttpPost("resend-new-device-otp")] public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) { - await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret); + var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException(); + if (!await _userService.VerifySecretAsync(user, request.Secret)) + { + await Task.Delay(2000); + throw new BadRequestException(string.Empty, "User verification failed."); + } + + await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user); } [HttpPost("verify-devices")] diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 83490f1c2f..58d909ddf6 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -7,6 +7,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -34,6 +35,7 @@ public class TwoFactorController : Controller private readonly IDuoUniversalTokenService _duoUniversalTokenService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; + private readonly ITwoFactorEmailService _twoFactorEmailService; public TwoFactorController( IUserService userService, @@ -44,7 +46,8 @@ public class TwoFactorController : Controller IVerifyAuthRequestCommand verifyAuthRequestCommand, IDuoUniversalTokenService duoUniversalConfigService, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, - IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) + IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector, + ITwoFactorEmailService twoFactorEmailService) { _userService = userService; _organizationRepository = organizationRepository; @@ -55,6 +58,7 @@ public class TwoFactorController : Controller _duoUniversalTokenService = duoUniversalConfigService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; + _twoFactorEmailService = twoFactorEmailService; } [HttpGet("")] @@ -297,8 +301,9 @@ public class TwoFactorController : Controller public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model) { var user = await CheckAsync(model, false, true); + // Add email to the user's 2FA providers, with the email address they've provided. model.ToUser(user); - await _userService.SendTwoFactorEmailAsync(user, false); + await _twoFactorEmailService.SendTwoFactorSetupEmailAsync(user); } [AllowAnonymous] @@ -316,15 +321,14 @@ public class TwoFactorController : Controller .VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId), requestModel.AuthRequestAccessCode)) { - await _userService.SendTwoFactorEmailAsync(user); - return; + await _twoFactorEmailService.SendTwoFactorEmailAsync(user); } } else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken)) { if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user)) { - await _userService.SendTwoFactorEmailAsync(user); + await _twoFactorEmailService.SendTwoFactorEmailAsync(user); return; } @@ -333,7 +337,7 @@ public class TwoFactorController : Controller } else if (await _userService.VerifySecretAsync(user, requestModel.Secret)) { - await _userService.SendTwoFactorEmailAsync(user); + await _twoFactorEmailService.SendTwoFactorEmailAsync(user); return; } } diff --git a/src/Core/Auth/Enums/TwoFactorEmailPurpose.cs b/src/Core/Auth/Enums/TwoFactorEmailPurpose.cs new file mode 100644 index 0000000000..47ac2e0e3c --- /dev/null +++ b/src/Core/Auth/Enums/TwoFactorEmailPurpose.cs @@ -0,0 +1,8 @@ +namespace Core.Auth.Enums; + +public enum TwoFactorEmailPurpose +{ + Login, + Setup, + NewDeviceVerification, +} diff --git a/src/Core/Auth/Services/ITwoFactorEmailService.cs b/src/Core/Auth/Services/ITwoFactorEmailService.cs new file mode 100644 index 0000000000..b0d0de6b01 --- /dev/null +++ b/src/Core/Auth/Services/ITwoFactorEmailService.cs @@ -0,0 +1,11 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Auth.Services; + +public interface ITwoFactorEmailService +{ + Task SendTwoFactorEmailAsync(User user); + Task SendTwoFactorSetupEmailAsync(User user); + Task SendNewDeviceVerificationEmailAsync(User user); + Task VerifyTwoFactorTokenAsync(User user, string token); +} diff --git a/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs new file mode 100644 index 0000000000..817b92729b --- /dev/null +++ b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs @@ -0,0 +1,116 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Bit.Core.Auth.Enums; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Core.Auth.Enums; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.Auth.Services; + +public class TwoFactorEmailService : ITwoFactorEmailService +{ + private readonly ICurrentContext _currentContext; + private readonly UserManager _userManager; + private readonly IMailService _mailService; + + public TwoFactorEmailService( + ICurrentContext currentContext, + IMailService mailService, + UserManager userManager + ) + { + _currentContext = currentContext; + _userManager = userManager; + _mailService = mailService; + } + + /// + /// Sends a two-factor email to the user with an OTP token for login + /// + /// The user to whom the email should be sent + /// Thrown if the user does not have an email for email 2FA + public async Task SendTwoFactorEmailAsync(User user) + { + await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Login); + } + + /// + /// Sends a two-factor email to the user with an OTP for setting up 2FA + /// + /// The user to whom the email should be sent + /// Thrown if the user does not have an email for email 2FA + public async Task SendTwoFactorSetupEmailAsync(User user) + { + await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Setup); + } + + /// + /// Sends a new device verification email to the user with an OTP token + /// + /// The user to whom the email should be sent + /// Thrown if the user is not provided + public async Task SendNewDeviceVerificationEmailAsync(User user) + { + ArgumentNullException.ThrowIfNull(user); + + var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider, + "otp:" + user.Email); + + var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; + + await _mailService.SendTwoFactorEmailAsync( + user.Email, user.Email, token, _currentContext.IpAddress, deviceType, TwoFactorEmailPurpose.NewDeviceVerification); + } + + /// + /// Verifies the two-factor token for the specified user + /// + /// The user for whom the token should be verified + /// The token to verify + /// Thrown if the user does not have an email for email 2FA + public async Task VerifyTwoFactorTokenAsync(User user, string token) + { + var email = GetUserTwoFactorEmail(user); + return await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token); + } + + /// + /// Sends a two-factor email with the specified purpose to the user only if they have 2FA email set up + /// + /// The user to whom the email should be sent + /// The purpose of the email + /// Thrown if the user does not have an email set up for 2FA + private async Task VerifyAndSendTwoFactorEmailAsync(User user, TwoFactorEmailPurpose purpose) + { + var email = GetUserTwoFactorEmail(user); + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)); + + var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) + .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; + + await _mailService.SendTwoFactorEmailAsync( + email, user.Email, token, _currentContext.IpAddress, deviceType, purpose); + } + + /// + /// Verifies the user has email 2FA and will return the email if present and throw otherwise. + /// + /// The user to check + /// The user's 2FA email address + /// + private string GetUserTwoFactorEmail(User user) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue)) + { + throw new ArgumentNullException("No email."); + } + return ((string)emailValue).ToLowerInvariant(); + } +} diff --git a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs index 27a222f1de..7add179787 100644 --- a/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Auth/TwoFactorEmail.html.hbs @@ -12,7 +12,9 @@
  • Deauthorize unrecognized devices
  • Change your master password
  • -
  • Turn on two-step login
  • + {{#if DisplayTwoFactorReminder}} +
  • Turn on two-step login
  • + {{/if}}
diff --git a/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs index dbd47af35a..20c340acda 100644 --- a/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs +++ b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs @@ -22,4 +22,9 @@ public class TwoFactorEmailTokenViewModel : BaseMailModel public string TimeZone { get; set; } public string DeviceIp { get; set; } public string DeviceType { get; set; } + /// + /// Depending on the context, we may want to show a reminder to the user that they should enable two factor authentication. + /// This is not relevant when the user is using the email to verify setting up 2FA, so we hide it in that case. + /// + public bool DisplayTwoFactorReminder { get; set; } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index aa1c0c8c25..e5a7577770 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -8,6 +8,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; using Bit.Core.Vault.Models.Data; +using Core.Auth.Enums; namespace Bit.Core.Services; @@ -27,7 +28,7 @@ public interface IMailService Task SendCannotDeleteClaimedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); Task SendChangeEmailEmailAsync(string newEmailAddress, string token); - Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true); + Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index e63b4e3b87..2ac9345ebf 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -21,21 +21,6 @@ public interface IUserService Task CreateUserAsync(User user); Task CreateUserAsync(User user, string masterPasswordHash); Task SendMasterPasswordHintAsync(string email); - /// - /// Used for both email two factor and email two factor setup. - /// - /// user requesting the action - /// this controls if what verbiage is shown in the email - /// void - Task SendTwoFactorEmailAsync(User user, bool authentication = true); - /// - /// Calls the same email implementation but instead it sends the token to the account email not the - /// email set up for two-factor, since in practice they can be different. - /// - /// user attepting to login with a new device - /// void - Task SendNewDeviceVerificationEmailAsync(User user); - Task VerifyTwoFactorEmailAsync(User user, string token); Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); @@ -87,7 +72,6 @@ public interface IUserService Task SendOTPAsync(User user); Task VerifyOTPAsync(User user, string token); Task VerifySecretAsync(User user, string secret, bool isSettingMFA = false); - Task ResendNewDeviceVerificationEmail(string email, string secret); /// /// We use this method to check if the user has an active new device verification bypass /// diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 20f6e3a0ab..254a0dd841 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -21,6 +21,7 @@ using Bit.Core.SecretsManager.Models.Mail; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; +using Core.Auth.Enums; using HandlebarsDotNet; namespace Bit.Core.Services; @@ -166,14 +167,14 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true) + public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose) { var message = CreateDefaultMessage("Your Bitwarden Verification Code", email); var requestDateTime = DateTime.UtcNow; var model = new TwoFactorEmailTokenViewModel { Token = token, - EmailTotpAction = authentication ? "logging in" : "setting up two-step login", + EmailTotpAction = (purpose == TwoFactorEmailPurpose.Setup) ? "setting up two-step login" : "logging in", AccountEmail = accountEmail, TheDate = requestDateTime.ToLongDateString(), TheTime = requestDateTime.ToShortTimeString(), @@ -182,6 +183,9 @@ public class HandlebarsMailService : IMailService DeviceType = deviceType, WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, + // We only want to remind users to set up 2FA if they're getting a new device verification email. + // For login with 2FA, and setup of 2FA, we do not want to show the reminder because users are already doing so. + DisplayTwoFactorReminder = purpose == TwoFactorEmailPurpose.NewDeviceVerification }; await AddMessageContentAsync(message, "Auth.TwoFactorEmail", model); message.MetaData.Add("SendGridBypassListManagement", true); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index fe5a064c44..1e8acf0a15 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; -using System.Reflection; -using System.Security.Claims; +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; @@ -337,52 +335,6 @@ public class UserService : UserManager, IUserService await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint); } - public async Task SendTwoFactorEmailAsync(User user, bool authentication = true) - { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue)) - { - throw new ArgumentNullException("No email."); - } - - var email = ((string)emailValue).ToLowerInvariant(); - var token = await base.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)); - - var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; - - await _mailService.SendTwoFactorEmailAsync( - email, user.Email, token, _currentContext.IpAddress, deviceType, authentication); - } - - public async Task SendNewDeviceVerificationEmailAsync(User user) - { - ArgumentNullException.ThrowIfNull(user); - - var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider, - "otp:" + user.Email); - - var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString()) - .FirstOrDefault()?.GetCustomAttribute()?.GetName() ?? "Unknown Browser"; - - await _mailService.SendTwoFactorEmailAsync( - user.Email, user.Email, token, _currentContext.IpAddress, deviceType); - } - - public async Task VerifyTwoFactorEmailAsync(User user, string token) - { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue)) - { - throw new ArgumentNullException("No email."); - } - - var email = ((string)emailValue).ToLowerInvariant(); - return await base.VerifyTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token); - } - public async Task StartWebAuthnRegistrationAsync(User user) { var providers = user.GetTwoFactorProviders(); @@ -1454,20 +1406,6 @@ public class UserService : UserManager, IUserService return isVerified; } - public async Task ResendNewDeviceVerificationEmail(string email, string secret) - { - var user = await _userRepository.GetByEmailAsync(email); - if (user == null) - { - return; - } - - if (await VerifySecretAsync(user, secret)) - { - await SendNewDeviceVerificationEmailAsync(user); - } - } - public async Task ActiveNewDeviceVerificationException(Guid userId) { var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString()); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 26858911a8..d8f2488088 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -8,6 +8,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; using Bit.Core.Vault.Models.Data; +using Core.Auth.Enums; namespace Bit.Core.Services; @@ -86,7 +87,7 @@ public class NoopMailService : IMailService public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) => Task.CompletedTask; - public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true) + public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose) { return Task.FromResult(0); } diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 4dc77c4449..ce5189703e 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -1,6 +1,7 @@ using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core; +using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -22,6 +23,7 @@ public class DeviceValidator( ICurrentContext currentContext, IUserService userService, IDistributedCache distributedCache, + ITwoFactorEmailService twoFactorEmailService, ILogger logger) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; @@ -32,6 +34,7 @@ public class DeviceValidator( private readonly IUserService _userService = userService; private readonly IDistributedCache distributedCache = distributedCache; private readonly ILogger _logger = logger; + private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService; public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { @@ -75,7 +78,7 @@ public class DeviceValidator( BuildDeviceErrorResult(validationResult); if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired) { - await _userService.SendNewDeviceVerificationEmailAsync(context.User); + await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(context.User); } return false; } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 8534fc0d5c..1ff7943378 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -234,6 +234,7 @@ public static class ServiceCollectionExtensions }); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 64261ede82..ce870c0860 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; @@ -31,6 +32,8 @@ public class AccountsControllerTests : IDisposable private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; + private readonly ITwoFactorEmailService _twoFactorEmailService; + public AccountsControllerTests() { @@ -43,6 +46,8 @@ public class AccountsControllerTests : IDisposable _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); + _twoFactorEmailService = Substitute.For(); + _sut = new AccountsController( _organizationService, @@ -53,7 +58,8 @@ public class AccountsControllerTests : IDisposable _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, _twoFactorIsEnabledQuery, - _featureService + _featureService, + _twoFactorEmailService ); } @@ -547,6 +553,46 @@ public class AccountsControllerTests : IDisposable Assert.Equal(model.VerifyDevices, user.VerifyDevices); } + [Theory] + [BitAutoData] + public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail( + UnauthenticatedSecretVerificationRequestModel model) + { + // Arrange + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult((User)null)); + + // Act & Assert + await Assert.ThrowsAsync(() => _sut.ResendNewDeviceOtpAsync(model)); + } + + [Theory, BitAutoData] + public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail( + User user, + UnauthenticatedSecretVerificationRequestModel model) + { + // Arrange + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + _userService.VerifySecretAsync(user, Arg.Any()).Returns(Task.FromResult(false)); + + // Act & Assert + await Assert.ThrowsAsync(() => _sut.ResendNewDeviceOtpAsync(model)); + } + + [Theory, BitAutoData] + public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user, + UnauthenticatedSecretVerificationRequestModel model) + { + // Arrange + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + _userService.VerifySecretAsync(user, Arg.Any()).Returns(Task.FromResult(true)); + + // Act + await _sut.ResendNewDeviceOtpAsync(model); + + // Assert + await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user); + } + // Below are helper functions that currently belong to this // test class, but ultimately may need to be split out into // something greater in order to share common test steps with diff --git a/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs b/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs new file mode 100644 index 0000000000..e3787409b8 --- /dev/null +++ b/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs @@ -0,0 +1,254 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Core.Auth.Enums; +using Microsoft.AspNetCore.Identity; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.Services; + +[SutProviderCustomize] +public class TwoFactorEmailServiceTests +{ + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_Success(SutProvider sutProvider, User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + var deviceType = DeviceType.Android; + + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = IpAddress; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("TwoFactor", Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider); + + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["Email"] = email }, + Enabled = true + } + }); + await sutProvider.Sut.SendTwoFactorEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(), + TwoFactorEmailPurpose.Login); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorSetupEmailAsync_Success(SutProvider sutProvider, User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + var deviceType = DeviceType.Android; + + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = IpAddress; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("TwoFactor", Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider); + + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["Email"] = email }, + Enabled = true + } + }); + + await sutProvider.Sut.SendTwoFactorSetupEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(), + TwoFactorEmailPurpose.Setup); + } + + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_Success(SutProvider sutProvider, User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + var deviceType = DeviceType.Android; + + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = IpAddress; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("otp:" + user.Email, Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, userTwoFactorTokenProvider); + + await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(), + TwoFactorEmailPurpose.NewDeviceVerification); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider sutProvider, User user) + { + user.TwoFactorProviders = null; + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider sutProvider, User user) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = null, + Enabled = true + } + }); + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider sutProvider, User user) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["qweqwe"] = user.Email.ToLowerInvariant() }, + Enabled = true + } + }); + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null)); + } + + [Theory] + [BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")] + [BitAutoData(DeviceType.Android, "Android")] + public async Task SendTwoFactorEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName, + SutProvider sutProvider, + User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = IpAddress; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("TwoFactor", Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider); + + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["Email"] = email }, + Enabled = true + } + }); + + await sutProvider.Sut.SendTwoFactorEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), deviceTypeName, TwoFactorEmailPurpose.Login); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider sutProvider, User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("TwoFactor", Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var context = Substitute.For(); + context.DeviceType = null; + context.IpAddress = IpAddress; + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider); + + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["Email"] = email }, + Enabled = true + } + }); + + await sutProvider.Sut.SendTwoFactorEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), "Unknown Browser", Arg.Any()); + } +} diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 0589753dd7..5332ae21de 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -11,7 +11,6 @@ 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.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -84,125 +83,6 @@ public class UserServiceTests Assert.Equal(1, versionProp.GetInt32()); } - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_Success(SutProvider sutProvider, User user) - { - var email = user.Email.ToLowerInvariant(); - var token = "thisisatokentocompare"; - var authentication = true; - var IpAddress = "1.1.1.1"; - var deviceType = "Android"; - - var userTwoFactorTokenProvider = Substitute.For>(); - userTwoFactorTokenProvider - .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) - .Returns(Task.FromResult(true)); - userTwoFactorTokenProvider - .GenerateAsync("TwoFactor", Arg.Any>(), user) - .Returns(Task.FromResult(token)); - - var context = sutProvider.GetDependency(); - context.DeviceType = DeviceType.Android; - context.IpAddress = IpAddress; - - sutProvider.Sut.RegisterTokenProvider("Custom_Email", userTwoFactorTokenProvider); - - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = new Dictionary { ["Email"] = email }, - Enabled = true - } - }); - await sutProvider.Sut.SendTwoFactorEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType, authentication); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider sutProvider, User user) - { - user.TwoFactorProviders = null; - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider sutProvider, User user) - { - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = null, - Enabled = true - } - }); - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider sutProvider, User user) - { - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = new Dictionary { ["qweqwe"] = user.Email.ToLowerInvariant() }, - Enabled = true - } - }); - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider sutProvider) - { - await Assert.ThrowsAsync(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null)); - } - - [Theory] - [BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")] - [BitAutoData(DeviceType.Android, "Android")] - public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName, - User user) - { - var sutProvider = new SutProvider() - .CreateWithUserServiceCustomizations(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = deviceType; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), deviceTypeName, Arg.Any()); - } - - [Theory, BitAutoData] - public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user) - { - var sutProvider = new SutProvider() - .CreateWithUserServiceCustomizations(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = null; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), "Unknown Browser", Arg.Any()); - } - [Theory, BitAutoData] public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider sutProvider, User user) { @@ -577,78 +457,6 @@ public class UserServiceTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); } - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( - SutProvider sutProvider, string email, string secret) - { - sutProvider.GetDependency() - .GetByEmailAsync(email) - .Returns(null as User); - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendTwoFactorEmailAsyncNotCalled( - SutProvider sutProvider, string email, string secret) - { - sutProvider.GetDependency() - .GetByEmailAsync(email) - .Returns(null as User); - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user) - { - // Arrange - var testPassword = "test_password"; - SetupUserAndDevice(user, true); - - var sutProvider = new SutProvider() - .CreateWithUserServiceCustomizations(user); - - // Setup the fake password verification - sutProvider - .GetDependency>() - .GetPasswordHashAsync(user, Arg.Any()) - .Returns((ci) => - { - return Task.FromResult("hashed_test_password"); - }); - - sutProvider.GetDependency>() - .VerifyHashedPassword(user, "hashed_test_password", testPassword) - .Returns((ci) => - { - return PasswordVerificationResult.Success; - }); - - sutProvider.GetDependency() - .GetByEmailAsync(user.Email) - .Returns(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = DeviceType.Android; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - - } - [Theory] [BitAutoData("")] [BitAutoData("null")] diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 9e20e630cd..9058d26cf1 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Context; +using Bit.Core.Auth.Services; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; @@ -26,6 +27,7 @@ public class DeviceValidatorTests private readonly ICurrentContext _currentContext; private readonly IUserService _userService; private readonly IDistributedCache _distributedCache; + private readonly ITwoFactorEmailService _twoFactorEmailService; private readonly Logger _logger; private readonly DeviceValidator _sut; @@ -39,6 +41,7 @@ public class DeviceValidatorTests _currentContext = Substitute.For(); _userService = Substitute.For(); _distributedCache = Substitute.For(); + _twoFactorEmailService = Substitute.For(); _logger = new Logger(Substitute.For()); _sut = new DeviceValidator( _deviceService, @@ -48,6 +51,7 @@ public class DeviceValidatorTests _currentContext, _userService, _distributedCache, + _twoFactorEmailService, _logger); } @@ -580,7 +584,7 @@ public class DeviceValidatorTests var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert - await _userService.Received(1).SendNewDeviceVerificationEmailAsync(context.User); + await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(context.User); await _deviceService.Received(0).SaveAsync(Arg.Any()); Assert.False(result); From 737f549f8297709b9de487bf9b289e2584dd2329 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:52:30 -0400 Subject: [PATCH 022/326] feat(otp): [PM-18612] Consolidate all email OTP to use 6 digits * Change email OTP to 6 digits * Added comment on base class --- .../Identity/TokenProviders/EmailTokenProvider.cs | 5 ++++- .../TokenProviders/EmailTwoFactorTokenProvider.cs | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index be94124c03..9481710390 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -7,6 +7,9 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; +/// +/// Generates and validates tokens for email OTPs. +/// public class EmailTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "EmailToken_{0}_{1}_{2}"; @@ -25,7 +28,7 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider }; } - public int TokenLength { get; protected set; } = 8; + public int TokenLength { get; protected set; } = 6; public bool TokenAlpha { get; protected set; } = false; public bool TokenNumeric { get; protected set; } = true; diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 2f8481cea2..3101974b94 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -7,17 +7,18 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; +/// +/// Generates tokens for email two-factor authentication. +/// It inherits from the EmailTokenProvider class, which manages the persistence and validation of tokens, +/// and adds additional validation to ensure that 2FA is enabled for the user. +/// public class EmailTwoFactorTokenProvider : EmailTokenProvider { public EmailTwoFactorTokenProvider( [FromKeyedServices("persistent")] IDistributedCache distributedCache) : base(distributedCache) - { - TokenAlpha = false; - TokenNumeric = true; - TokenLength = 6; - } + { } public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { From af75fdbe3604a054bbb9c9cfc4ad280d3e8a0f06 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:07:02 -0400 Subject: [PATCH 023/326] [PM-21370] Update Github Action grouping (#5790) * Update Github Action grouping * Undid codeowners change. --- .github/renovate.json5 | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5c1b259539..36f793e8c1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -9,18 +9,6 @@ "nuget", ], packageRules: [ - { - // Group all release-related workflows for GitHub Actions together for BRE. - groupName: "github-action", - matchManagers: ["github-actions"], - matchFileNames: [ - ".github/workflows/publish.yml", - ".github/workflows/release.yml" - ], - commitMessagePrefix: "[deps] BRE:", - reviewers: ["team:dept-bre"], - addLabels: ["hold"], - }, { groupName: "dockerfile minor", matchManagers: ["dockerfile"], @@ -35,6 +23,7 @@ groupName: "github-action minor", matchManagers: ["github-actions"], matchUpdateTypes: ["minor"], + addLabels: ["hold"], }, { // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. From ead29eed7a442c906a87d5802c494cd7e7fd68ae Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:38:52 -0400 Subject: [PATCH 024/326] chore(feature flag): [PM-18562] Remove installation-last-activity-date from server * Removed flag. * Changed to remove variable. --- src/Core/Constants.cs | 1 - .../RequestValidators/CustomTokenRequestValidator.cs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b2e28dab47..f2039cfbc9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -189,7 +189,6 @@ public static class FeatureFlagKeys public const string PersistPopupView = "persist-popup-view"; public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string WebPush = "web-push"; - public const string RecordInstallationLastActivityDate = "installation-last-activity-date"; public const string IpcChannelFramework = "ipc-channel-framework"; /* Tools Team */ diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 7d468fafa8..5042f38b4f 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -95,10 +95,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator { { "encrypted_payload", payload } }; } - if (FeatureService.IsEnabled(FeatureFlagKeys.RecordInstallationLastActivityDate) - && context.Result.ValidatedRequest.ClientId.StartsWith("installation")) + if (context.Result.ValidatedRequest.ClientId.StartsWith("installation")) { - var installationIdPart = clientId.Split(".")[1]; await RecordActivityForInstallation(clientId.Split(".")[1]); } return; From 799327e933f0d21d515731d9823e08a5158a8157 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:15:56 -0400 Subject: [PATCH 025/326] [deps] DbOps: Update Microsoft.Azure.Cosmos to 3.52.0 (#6044) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 85251b3185..e1c476bebc 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -36,7 +36,7 @@ - + From b61063ceb434f999c8bc69f5ecf9c1d6e8eb0590 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 8 Jul 2025 08:54:53 -0400 Subject: [PATCH 026/326] Changing seat count for validating secrets manager. (#6035) --- .../Validation/InviteOrganizationUserValidator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 837ac6f055..557ece2104 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -63,9 +63,12 @@ public class InviteOrganizationUsersValidator( { try { + var organization = await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId); + + organization!.Seats = subscriptionUpdate.UpdatedSeatTotal; var smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate( - organization: await organizationRepository.GetByIdAsync(request.InviteOrganization.OrganizationId), + organization: organization, plan: request.InviteOrganization.Plan, autoscaling: true); From 7fb7d6fa564f45ae913a79151db25b3be1a5757a Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:25:41 -0400 Subject: [PATCH 027/326] Add `#nullable disable` to auth code (#6055) --- bitwarden_license/src/Sso/Controllers/AccountController.cs | 5 ++++- bitwarden_license/src/Sso/Controllers/HomeController.cs | 5 ++++- bitwarden_license/src/Sso/Models/ErrorViewModel.cs | 5 ++++- bitwarden_license/src/Sso/Models/RedirectViewModel.cs | 5 ++++- bitwarden_license/src/Sso/Models/SamlEnvironment.cs | 5 ++++- bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs | 5 ++++- .../src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs | 5 ++++- .../src/Sso/Utilities/ExtendedOptionsMonitorCache.cs | 5 ++++- .../src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs | 5 ++++- .../src/Sso/Utilities/Saml2OptionsExtensions.cs | 5 ++++- .../src/Sso/Utilities/ServiceCollectionExtensions.cs | 5 ++++- src/Admin/Auth/Controllers/LoginController.cs | 5 ++++- src/Admin/Auth/Models/LoginModel.cs | 5 ++++- src/Api/Auth/Controllers/AccountsController.cs | 5 ++++- src/Api/Auth/Controllers/AuthRequestsController.cs | 5 ++++- src/Api/Auth/Controllers/EmergencyAccessController.cs | 5 ++++- src/Api/Auth/Controllers/TwoFactorController.cs | 5 ++++- src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs | 5 ++++- src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs | 5 ++++- .../Models/Request/Accounts/DeleteRecoverRequestModel.cs | 5 ++++- src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/EmailTokenRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/PasswordHintRequestModel.cs | 5 ++++- src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs | 5 ++++- .../Request/Accounts/RegenerateTwoFactorRequestModel.cs | 5 ++++- .../Request/Accounts/SecretVerificationRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/SetPasswordRequestModel.cs | 5 ++++- .../UnauthenticatedSecretVerificationRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs | 5 ++++- .../Models/Request/Accounts/UpdateProfileRequestModel.cs | 5 ++++- .../Accounts/UpdateTdeOffboardingPasswordRequestModel.cs | 5 ++++- .../Request/Accounts/UpdateTempPasswordRequestModel.cs | 5 ++++- .../Request/Accounts/VerifyDeleteRecoverRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs | 5 ++++- .../Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs | 5 ++++- src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs | 5 ++++- src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs | 5 ++++- src/Api/Auth/Models/Request/TwoFactorRequestModels.cs | 5 ++++- .../WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs | 5 ++++- .../WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs | 5 ++++- .../Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs | 5 ++++- src/Api/Auth/Models/Response/AuthRequestResponseModel.cs | 5 ++++- src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs | 5 ++++- src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs | 5 ++++- .../TwoFactor/TwoFactorAuthenticatorResponseModel.cs | 5 ++++- .../Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs | 5 ++++- .../Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs | 5 ++++- .../Response/TwoFactor/TwoFactorRecoverResponseModel.cs | 5 ++++- .../Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs | 5 ++++- .../Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs | 5 ++++- .../WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs | 5 ++++- src/Core/Auth/Entities/AuthRequest.cs | 5 ++++- src/Core/Auth/Entities/EmergencyAccess.cs | 5 ++++- src/Core/Auth/Entities/SsoConfig.cs | 5 ++++- src/Core/Auth/Entities/SsoUser.cs | 5 ++++- src/Core/Auth/Entities/WebAuthnCredential.cs | 5 ++++- src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs | 5 ++++- src/Core/Auth/Identity/RoleStore.cs | 5 ++++- .../Identity/TokenProviders/AuthenticatorTokenProvider.cs | 5 ++++- .../Identity/TokenProviders/DuoUniversalTokenProvider.cs | 5 ++++- .../Auth/Identity/TokenProviders/DuoUniversalTokenService.cs | 5 ++++- .../Identity/TokenProviders/EmailTwoFactorTokenProvider.cs | 5 ++++- .../TokenProviders/OrganizationDuoUniversalTokenProvider.cs | 5 ++++- .../Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs | 5 ++++- .../Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs | 5 ++++- src/Core/Auth/Identity/UserStore.cs | 5 ++++- src/Core/Auth/IdentityServer/TokenRetrieval.cs | 5 ++++- .../Auth/Models/Api/Request/Accounts/KeysRequestModel.cs | 5 ++++- .../Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs | 5 ++++- .../Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs | 5 ++++- .../Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs | 5 ++++- .../Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs | 5 ++++- .../Models/Api/Response/DeviceAuthRequestResponseModel.cs | 5 ++++- .../Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs | 5 ++++- .../Business/Tokenables/EmergencyAccessInviteTokenable.cs | 5 ++++- .../Models/Business/Tokenables/OrgUserInviteTokenable.cs | 5 ++++- .../Tokenables/RegistrationEmailVerificationTokenable.cs | 5 ++++- .../Business/Tokenables/SsoEmail2faSessionTokenable.cs | 5 ++++- src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs | 5 ++++- .../TwoFactorAuthenticatorUserVerificationTokenable.cs | 5 ++++- .../Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs | 5 ++++- .../Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs | 5 ++++- src/Core/Auth/Models/Data/EmergencyAccessDetails.cs | 5 ++++- src/Core/Auth/Models/Data/EmergencyAccessNotify.cs | 5 ++++- src/Core/Auth/Models/Data/EmergencyAccessViewData.cs | 5 ++++- src/Core/Auth/Models/Data/GrantItem.cs | 5 ++++- src/Core/Auth/Models/Data/IGrant.cs | 5 ++++- src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs | 5 ++++- src/Core/Auth/Models/Data/SsoConfigurationData.cs | 5 ++++- src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs | 5 ++++- src/Core/Auth/Models/ITwoFactorProvidersUser.cs | 5 ++++- .../Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs | 5 ++++- .../Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs | 5 ++++- .../Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs | 5 ++++- src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs | 5 ++++- .../Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs | 5 ++++- .../Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs | 5 ++++- .../Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs | 5 ++++- src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs | 5 ++++- src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs | 5 ++++- src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs | 5 ++++- src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs | 5 ++++- src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs | 5 ++++- src/Core/Auth/Models/Mail/VerifyDeleteModel.cs | 5 ++++- src/Core/Auth/Models/Mail/VerifyEmailModel.cs | 5 ++++- src/Core/Auth/Models/TwoFactorProvider.cs | 5 ++++- .../Auth/Services/EmergencyAccess/EmergencyAccessService.cs | 5 ++++- src/Core/Auth/Services/Implementations/SsoConfigService.cs | 5 ++++- .../Auth/Services/Implementations/TwoFactorEmailService.cs | 5 ++++- .../Registration/Implementations/RegisterUserCommand.cs | 5 ++++- .../UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs | 5 ++++- .../WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs | 5 ++++- .../Implementations/CreateWebAuthnLoginCredentialCommand.cs | 5 ++++- .../Identity/CustomIdentityServiceCollectionExtensions.cs | 5 ++++- .../ConfigureOpenIdConnectDistributedOptions.cs | 5 ++++- src/Core/IdentityServer/DistributedCacheCookieManager.cs | 5 ++++- .../IdentityServer/DistributedCacheTicketDataFormatter.cs | 5 ++++- src/Core/IdentityServer/DistributedCacheTicketStore.cs | 5 ++++- src/Identity/Controllers/AccountsController.cs | 5 ++++- src/Identity/Controllers/SsoController.cs | 5 ++++- src/Identity/IdentityServer/ApiClient.cs | 5 ++++- src/Identity/IdentityServer/AuthorizationCodeStore.cs | 5 ++++- .../ClientProviders/InstallationClientProvider.cs | 5 ++++- .../ClientProviders/OrganizationClientProvider.cs | 5 ++++- .../ClientProviders/SecretsManagerApiKeyProvider.cs | 5 ++++- src/Identity/IdentityServer/CustomValidatorRequestContext.cs | 5 ++++- src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs | 5 ++++- src/Identity/IdentityServer/PersistedGrantStore.cs | 5 ++++- src/Identity/IdentityServer/ProfileService.cs | 5 ++++- .../IdentityServer/RequestValidators/BaseRequestValidator.cs | 5 ++++- .../IdentityServer/RequestValidators/DeviceValidator.cs | 5 ++++- .../RequestValidators/ResourceOwnerPasswordValidator.cs | 5 ++++- .../RequestValidators/TwoFactorAuthenticationValidator.cs | 5 ++++- .../RequestValidators/WebAuthnGrantValidator.cs | 5 ++++- src/Identity/Models/RedirectViewModel.cs | 5 ++++- src/Identity/Models/Request/Accounts/PreloginRequestModel.cs | 5 ++++- src/Identity/Models/Request/Accounts/RegisterRequestModel.cs | 5 ++++- src/Identity/Program.cs | 5 ++++- .../Auth/Helpers/EmergencyAccessHelpers.cs | 5 ++++- .../Auth/Models/AuthRequest.cs | 5 ++++- .../Auth/Models/EmergencyAccess.cs | 5 ++++- src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs | 5 ++++- src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs | 5 ++++- .../Auth/Models/WebAuthnCredential.cs | 5 ++++- 144 files changed, 576 insertions(+), 144 deletions(-) diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 12394ff598..00657a4e7f 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; diff --git a/bitwarden_license/src/Sso/Controllers/HomeController.cs b/bitwarden_license/src/Sso/Controllers/HomeController.cs index 7be9d86215..da30d5106d 100644 --- a/bitwarden_license/src/Sso/Controllers/HomeController.cs +++ b/bitwarden_license/src/Sso/Controllers/HomeController.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using Bit.Sso.Models; using Duende.IdentityServer.Services; using Microsoft.AspNetCore.Authorization; diff --git a/bitwarden_license/src/Sso/Models/ErrorViewModel.cs b/bitwarden_license/src/Sso/Models/ErrorViewModel.cs index 1f6e9735e7..8efb95b09e 100644 --- a/bitwarden_license/src/Sso/Models/ErrorViewModel.cs +++ b/bitwarden_license/src/Sso/Models/ErrorViewModel.cs @@ -1,4 +1,7 @@ -using Duende.IdentityServer.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Duende.IdentityServer.Models; namespace Bit.Sso.Models; diff --git a/bitwarden_license/src/Sso/Models/RedirectViewModel.cs b/bitwarden_license/src/Sso/Models/RedirectViewModel.cs index 9bc294d96c..0e2a642207 100644 --- a/bitwarden_license/src/Sso/Models/RedirectViewModel.cs +++ b/bitwarden_license/src/Sso/Models/RedirectViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Sso.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Sso.Models; public class RedirectViewModel { diff --git a/bitwarden_license/src/Sso/Models/SamlEnvironment.cs b/bitwarden_license/src/Sso/Models/SamlEnvironment.cs index 6de718029a..0a7dbcd44b 100644 --- a/bitwarden_license/src/Sso/Models/SamlEnvironment.cs +++ b/bitwarden_license/src/Sso/Models/SamlEnvironment.cs @@ -1,4 +1,7 @@ -using System.Security.Cryptography.X509Certificates; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Cryptography.X509Certificates; namespace Bit.Sso.Models; diff --git a/bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs b/bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs index f82614635c..7c34217805 100644 --- a/bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/ClaimsExtensions.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using System.Text.RegularExpressions; namespace Bit.Sso.Utilities; diff --git a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs index 8bde8f84a1..c65d7435c3 100644 --- a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs +++ b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs @@ -1,4 +1,7 @@ -using System.Security.Cryptography.X509Certificates; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Cryptography.X509Certificates; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; diff --git a/bitwarden_license/src/Sso/Utilities/ExtendedOptionsMonitorCache.cs b/bitwarden_license/src/Sso/Utilities/ExtendedOptionsMonitorCache.cs index 083417f25b..4f95e4bf39 100644 --- a/bitwarden_license/src/Sso/Utilities/ExtendedOptionsMonitorCache.cs +++ b/bitwarden_license/src/Sso/Utilities/ExtendedOptionsMonitorCache.cs @@ -1,4 +1,7 @@ -using System.Collections.Concurrent; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Collections.Concurrent; using Microsoft.Extensions.Options; namespace Bit.Sso.Utilities; diff --git a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs index 825ed74dc8..199d8475a6 100644 --- a/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/OpenIdConnectOptionsExtensions.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Authentication.OpenIdConnect; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace Bit.Sso.Utilities; diff --git a/bitwarden_license/src/Sso/Utilities/Saml2OptionsExtensions.cs b/bitwarden_license/src/Sso/Utilities/Saml2OptionsExtensions.cs index 46a75ca5c2..55ee63e91a 100644 --- a/bitwarden_license/src/Sso/Utilities/Saml2OptionsExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/Saml2OptionsExtensions.cs @@ -1,4 +1,7 @@ -using System.IO.Compression; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.IO.Compression; using System.Text; using System.Xml; using Sustainsys.Saml2; diff --git a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs index b1c0a55cbe..a51a04f5c8 100644 --- a/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Sso/Utilities/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using Bit.Core.Business.Sso; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Business.Sso; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; diff --git a/src/Admin/Auth/Controllers/LoginController.cs b/src/Admin/Auth/Controllers/LoginController.cs index dbc04e96c0..7be161e6d9 100644 --- a/src/Admin/Auth/Controllers/LoginController.cs +++ b/src/Admin/Auth/Controllers/LoginController.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Auth.IdentityServer; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Admin.Auth.IdentityServer; using Bit.Admin.Auth.Models; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; diff --git a/src/Admin/Auth/Models/LoginModel.cs b/src/Admin/Auth/Models/LoginModel.cs index 7dd8521a4f..2a1eab0d7c 100644 --- a/src/Admin/Auth/Models/LoginModel.cs +++ b/src/Admin/Auth/Models/LoginModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Admin.Auth.Models; diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 695042bda7..f197f1270b 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index d1d6a8a524..3f91bd6eea 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Auth.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.Auth.Enums; diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 8b40444634..53b57fe685 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Response; diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 58d909ddf6..96b64f16fc 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Auth.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Response.TwoFactor; using Bit.Api.Models.Request; diff --git a/src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs b/src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs index 4ab1f24287..c67cb9db3f 100644 --- a/src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs +++ b/src/Api/Auth/Jobs/EmergencyAccessNotificationJob.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Services; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Services; using Bit.Core.Jobs; using Quartz; diff --git a/src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs b/src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs index b125e4f057..f23774f060 100644 --- a/src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs +++ b/src/Api/Auth/Jobs/EmergencyAccessTimeoutJob.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Services; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Services; using Bit.Core.Jobs; using Quartz; diff --git a/src/Api/Auth/Models/Request/Accounts/DeleteRecoverRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/DeleteRecoverRequestModel.cs index f4326ee6b6..a87836eff9 100644 --- a/src/Api/Auth/Models/Request/Accounts/DeleteRecoverRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/DeleteRecoverRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs index 8d45ec41b3..de90b3e83e 100644 --- a/src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/EmailRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/EmailTokenRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/EmailTokenRequestModel.cs index bd75b65a5e..ec5f4a27e1 100644 --- a/src/Api/Auth/Models/Request/Accounts/EmailTokenRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/EmailTokenRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/PasswordHintRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/PasswordHintRequestModel.cs index a52b7b5163..1f2bccd1ce 100644 --- a/src/Api/Auth/Models/Request/Accounts/PasswordHintRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/PasswordHintRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs index ce197c4aad..01da1f0f9f 100644 --- a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs index dbbcdf7331..e59001c203 100644 --- a/src/Api/Auth/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs index c0191728f4..7e4ce98fa2 100644 --- a/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/SecretVerificationRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs index b07c7ea81f..0d809c6c11 100644 --- a/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/SetPasswordRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificationRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificationRequestModel.cs index abd37023c8..bf0cbd76ec 100644 --- a/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificationRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UnauthenticatedSecretVerificationRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs index d3cb5c2442..ad35d98750 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Tools.Models.Request; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateProfileRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateProfileRequestModel.cs index 76072cb3a4..29ac9c5df9 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateProfileRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateProfileRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateTdeOffboardingPasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateTdeOffboardingPasswordRequestModel.cs index e246a99c96..e99c990756 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateTdeOffboardingPasswordRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateTdeOffboardingPasswordRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateTempPasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateTempPasswordRequestModel.cs index d2b8dce727..e071726edf 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateTempPasswordRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateTempPasswordRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Models.Request.Organizations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/VerifyDeleteRecoverRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/VerifyDeleteRecoverRequestModel.cs index 495cd0bdb5..3decedb14d 100644 --- a/src/Api/Auth/Models/Request/Accounts/VerifyDeleteRecoverRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/VerifyDeleteRecoverRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs index 730e3ee3be..8d086781d9 100644 --- a/src/Api/Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/VerifyEmailRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs index 1db68acc99..edfa3ce2b2 100644 --- a/src/Api/Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/VerifyOTPRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Auth.Models.Request.Accounts; diff --git a/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs b/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs index 8b1d5e883b..33a7e52791 100644 --- a/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs +++ b/src/Api/Auth/Models/Request/EmergencyAccessRequestModels.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Utilities; diff --git a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs index d82b26aa26..fcf386d7ee 100644 --- a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs +++ b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index 8d7df4160d..79df29c928 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs index 8c6acbc8d4..c73bd94292 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Fido2NetLib; diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs index 54244c2dbd..aaae88bd49 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Fido2NetLib; diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs index 7e161cfbea..ec4f2b1724 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Models.Data; using Bit.Core.Utilities; diff --git a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs index 7a9734d844..82aa38c9ac 100644 --- a/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs +++ b/src/Api/Auth/Models/Response/AuthRequestResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Auth.Entities; using Bit.Core.Enums; diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index 90b265715d..640c9bb3e0 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Vault.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Vault.Models.Response; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; diff --git a/src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs b/src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs index 0d327e1009..a8930bc9eb 100644 --- a/src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs +++ b/src/Api/Auth/Models/Response/OrganizationSsoResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Models.Api; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs index 71569174a7..47cf49c439 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Models.Api; using OtpNet; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index 79012783a4..e7e29d06cb 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs index d1d87d85b5..e16f2a6b78 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorEmailResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Models.Api; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorRecoverResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorRecoverResponseModel.cs index 0022633973..2369c0ea1c 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorRecoverResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorRecoverResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Api; namespace Bit.Api.Auth.Models.Response.TwoFactor; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs index 2e1d1aa050..cd853e5739 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorWebAuthnResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; using Bit.Core.Models.Api; diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs index 0a97367017..10cc6749e6 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Models.Api; diff --git a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs index d521bdac96..517785e6e4 100644 --- a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs +++ b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialCreateOptionsResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Fido2NetLib; namespace Bit.Api.Auth.Models.Response.WebAuthn; diff --git a/src/Core/Auth/Entities/AuthRequest.cs b/src/Core/Auth/Entities/AuthRequest.cs index 088c24b88a..af429adca2 100644 --- a/src/Core/Auth/Entities/AuthRequest.cs +++ b/src/Core/Auth/Entities/AuthRequest.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Entities/EmergencyAccess.cs b/src/Core/Auth/Entities/EmergencyAccess.cs index f295f25604..d855126468 100644 --- a/src/Core/Auth/Entities/EmergencyAccess.cs +++ b/src/Core/Auth/Entities/EmergencyAccess.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Entities/SsoConfig.cs b/src/Core/Auth/Entities/SsoConfig.cs index c872928031..bbe5e87962 100644 --- a/src/Core/Auth/Entities/SsoConfig.cs +++ b/src/Core/Auth/Entities/SsoConfig.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; namespace Bit.Core.Auth.Entities; diff --git a/src/Core/Auth/Entities/SsoUser.cs b/src/Core/Auth/Entities/SsoUser.cs index 2e457afbc6..eb3250f310 100644 --- a/src/Core/Auth/Entities/SsoUser.cs +++ b/src/Core/Auth/Entities/SsoUser.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; namespace Bit.Core.Auth.Entities; diff --git a/src/Core/Auth/Entities/WebAuthnCredential.cs b/src/Core/Auth/Entities/WebAuthnCredential.cs index 486fd41e3f..ecc763088d 100644 --- a/src/Core/Auth/Entities/WebAuthnCredential.cs +++ b/src/Core/Auth/Entities/WebAuthnCredential.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs b/src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs index 1318d94760..7f058ed5d4 100644 --- a/src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs +++ b/src/Core/Auth/Identity/LowerInvariantLookupNormalizer.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Identity; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.Identity; diff --git a/src/Core/Auth/Identity/RoleStore.cs b/src/Core/Auth/Identity/RoleStore.cs index 3ea530dd04..388f904e71 100644 --- a/src/Core/Auth/Identity/RoleStore.cs +++ b/src/Core/Auth/Identity/RoleStore.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.Identity; diff --git a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index 5a3d9522f3..6348d6f27b 100644 --- a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index 3f2a44915c..6ed715b14b 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Entities; diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs index 8dd07e7ee6..a59a76de0a 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 3101974b94..49a000a2bf 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; using Microsoft.AspNetCore.Identity; diff --git a/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs index c8007dd6ec..07768c32c9 100644 --- a/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 3b4b0fa520..60fb2c5635 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; diff --git a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index b33d2fc0c9..ddac1843ec 100644 --- a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/Auth/Identity/UserStore.cs b/src/Core/Auth/Identity/UserStore.cs index 41323f05b7..e8ae95a0bd 100644 --- a/src/Core/Auth/Identity/UserStore.cs +++ b/src/Core/Auth/Identity/UserStore.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; diff --git a/src/Core/Auth/IdentityServer/TokenRetrieval.cs b/src/Core/Auth/IdentityServer/TokenRetrieval.cs index 36c23506cb..bf0230bafb 100644 --- a/src/Core/Auth/IdentityServer/TokenRetrieval.cs +++ b/src/Core/Auth/IdentityServer/TokenRetrieval.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Http; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Http; namespace Bit.Core.Auth.IdentityServer; diff --git a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs index 0964fe1a1d..f89b67f3c5 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs b/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs index e7cd05be20..7fbc5f19b1 100644 --- a/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Enums; namespace Bit.Core.Auth.Models.Api.Request.AuthRequest; diff --git a/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs b/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs index 1577f3a1c8..c834ec8e55 100644 --- a/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Auth.Models.Api.Request.AuthRequest; diff --git a/src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs b/src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs index 111b03a3a3..bcd648d1fb 100644 --- a/src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/DeviceKeysUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Models/Api/Response/Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs b/src/Core/Auth/Models/Api/Response/Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs index 6a0641246b..6c323d6207 100644 --- a/src/Core/Auth/Models/Api/Response/Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs +++ b/src/Core/Auth/Models/Api/Response/Accounts/WebAuthnLoginAssertionOptionsResponseModel.cs @@ -1,4 +1,7 @@ - +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + + using Bit.Core.Models.Api; using Fido2NetLib; diff --git a/src/Core/Auth/Models/Api/Response/DeviceAuthRequestResponseModel.cs b/src/Core/Auth/Models/Api/Response/DeviceAuthRequestResponseModel.cs index 3e3076e84e..47a308b28d 100644 --- a/src/Core/Auth/Models/Api/Response/DeviceAuthRequestResponseModel.cs +++ b/src/Core/Auth/Models/Api/Response/DeviceAuthRequestResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Data; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs b/src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs index c64c552977..c2fff4afee 100644 --- a/src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs +++ b/src/Core/Auth/Models/Api/Response/ProtectedDeviceResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Core/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenable.cs index a29afdf1fb..8e8cc41653 100644 --- a/src/Core/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/EmergencyAccessInviteTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Auth.Entities; namespace Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs index 95c84ad3b5..f04a1181c4 100644 --- a/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Tokens; diff --git a/src/Core/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenable.cs index 006da70080..03fcb9b5c0 100644 --- a/src/Core/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/RegistrationEmailVerificationTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Tokens; namespace Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs index 30687a6a4a..eeffe0bedc 100644 --- a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Tokens; diff --git a/src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs index 48386c5439..06e2dda3d9 100644 --- a/src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/SsoTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Tokens; diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorAuthenticatorUserVerificationTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorAuthenticatorUserVerificationTokenable.cs index 70a94f5928..76e54374e0 100644 --- a/src/Core/Auth/Models/Business/Tokenables/TwoFactorAuthenticatorUserVerificationTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorAuthenticatorUserVerificationTokenable.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Tokens; using Newtonsoft.Json; diff --git a/src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs index e64edace45..049681a028 100644 --- a/src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/WebAuthnCredentialCreateOptionsTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Tokens; using Fido2NetLib; diff --git a/src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs index 017033b00a..bbea66a6b1 100644 --- a/src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/WebAuthnLoginAssertionOptionsTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Auth.Enums; using Bit.Core.Tokens; using Fido2NetLib; diff --git a/src/Core/Auth/Models/Data/EmergencyAccessDetails.cs b/src/Core/Auth/Models/Data/EmergencyAccessDetails.cs index 15ccad9cb1..03661c7276 100644 --- a/src/Core/Auth/Models/Data/EmergencyAccessDetails.cs +++ b/src/Core/Auth/Models/Data/EmergencyAccessDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Entities; namespace Bit.Core.Auth.Models.Data; diff --git a/src/Core/Auth/Models/Data/EmergencyAccessNotify.cs b/src/Core/Auth/Models/Data/EmergencyAccessNotify.cs index f3f1347338..1c0d4bfe8b 100644 --- a/src/Core/Auth/Models/Data/EmergencyAccessNotify.cs +++ b/src/Core/Auth/Models/Data/EmergencyAccessNotify.cs @@ -1,4 +1,7 @@ - +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + + using Bit.Core.Auth.Entities; namespace Bit.Core.Auth.Models.Data; diff --git a/src/Core/Auth/Models/Data/EmergencyAccessViewData.cs b/src/Core/Auth/Models/Data/EmergencyAccessViewData.cs index 70130e0fcf..1e5916d6af 100644 --- a/src/Core/Auth/Models/Data/EmergencyAccessViewData.cs +++ b/src/Core/Auth/Models/Data/EmergencyAccessViewData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Entities; using Bit.Core.Vault.Models.Data; namespace Bit.Core.Auth.Models.Data; diff --git a/src/Core/Auth/Models/Data/GrantItem.cs b/src/Core/Auth/Models/Data/GrantItem.cs index de856904db..6bf99c019b 100644 --- a/src/Core/Auth/Models/Data/GrantItem.cs +++ b/src/Core/Auth/Models/Data/GrantItem.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Auth.Repositories.Cosmos; using Duende.IdentityServer.Models; diff --git a/src/Core/Auth/Models/Data/IGrant.cs b/src/Core/Auth/Models/Data/IGrant.cs index 5f14631533..1465194a66 100644 --- a/src/Core/Auth/Models/Data/IGrant.cs +++ b/src/Core/Auth/Models/Data/IGrant.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Auth.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Auth.Models.Data; public interface IGrant { diff --git a/src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs b/src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs index cd3a98efcc..297f2d0120 100644 --- a/src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs +++ b/src/Core/Auth/Models/Data/OrganizationAdminAuthRequest.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// 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.Enums; namespace Bit.Core.Auth.Models.Data; diff --git a/src/Core/Auth/Models/Data/SsoConfigurationData.cs b/src/Core/Auth/Models/Data/SsoConfigurationData.cs index fe39a5a054..e4ff7af729 100644 --- a/src/Core/Auth/Models/Data/SsoConfigurationData.cs +++ b/src/Core/Auth/Models/Data/SsoConfigurationData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authentication.OpenIdConnect; diff --git a/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs index 40a096c474..5004d35e03 100644 --- a/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs +++ b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Core.Auth.Models.Data; diff --git a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs index f953e4570e..5cf137b76f 100644 --- a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs +++ b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Services; namespace Bit.Core.Auth.Models; diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs index afe29b9843..cbe6dbec1c 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessAcceptedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessAcceptedViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs index 9ad446aab6..65d80c06cb 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessApprovedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessApprovedViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs index 2ab55a05eb..4527dfddb0 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessConfirmedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessConfirmedViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs index fa432c5b70..5f9e450a0c 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessInvitedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessInvitedViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs index dd3ae3dd82..6d166b3ebb 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryTimedOutViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessRecoveryTimedOutViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs index 3811b49ff0..743a0707fc 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessRecoveryViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessRecoveryViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs b/src/Core/Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs index 101cb9c167..c704e121a3 100644 --- a/src/Core/Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs +++ b/src/Core/Auth/Models/Mail/EmergencyAccessRejectedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class EmergencyAccessRejectedViewModel : BaseMailModel { diff --git a/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs b/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs index 2d5bc7eb15..e7b0b042a5 100644 --- a/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs +++ b/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs b/src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs index e8ee28fc11..2c2f8343ea 100644 --- a/src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs +++ b/src/Core/Auth/Models/Mail/MasterPasswordHintViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs b/src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs index 4b195b54a8..e8a07a7ec5 100644 --- a/src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs +++ b/src/Core/Auth/Models/Mail/PasswordlessSignInModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Auth.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Auth.Models.Mail; public class PasswordlessSignInModel { diff --git a/src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs b/src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs index 82c2bc5303..ba11f3a442 100644 --- a/src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs +++ b/src/Core/Auth/Models/Mail/RecoverTwoFactorModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs b/src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs index f1863da691..fe42093111 100644 --- a/src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs +++ b/src/Core/Auth/Models/Mail/RegisterVerifyEmail.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/Mail/VerifyDeleteModel.cs b/src/Core/Auth/Models/Mail/VerifyDeleteModel.cs index d6b7b3a445..44b88a69a8 100644 --- a/src/Core/Auth/Models/Mail/VerifyDeleteModel.cs +++ b/src/Core/Auth/Models/Mail/VerifyDeleteModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/Mail/VerifyEmailModel.cs b/src/Core/Auth/Models/Mail/VerifyEmailModel.cs index 703de7e045..ea1d7f3398 100644 --- a/src/Core/Auth/Models/Mail/VerifyEmailModel.cs +++ b/src/Core/Auth/Models/Mail/VerifyEmailModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Auth/Models/TwoFactorProvider.cs b/src/Core/Auth/Models/TwoFactorProvider.cs index 04ef4d7cb2..9152769425 100644 --- a/src/Core/Auth/Models/TwoFactorProvider.cs +++ b/src/Core/Auth/Models/TwoFactorProvider.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Auth.Enums; using Fido2NetLib.Objects; diff --git a/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs index 6a8fe9dd17..4331179554 100644 --- a/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index bf7e2d56fe..fe8d9bdd6e 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; diff --git a/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs index 817b92729b..cb26e46cd5 100644 --- a/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs +++ b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Auth.Enums; using Bit.Core.Context; diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 289bbff7f8..991be2b764 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs index 8d4bd49e42..cc86d3d71d 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs index c25e226a32..61a573cb2d 100644 --- a/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/ICreateWebAuthnLoginCredentialCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Fido2NetLib; namespace Bit.Core.Auth.UserFeatures.WebAuthnLogin; diff --git a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs index 65c98dea3b..795fa95b9d 100644 --- a/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs +++ b/src/Core/Auth/UserFeatures/WebAuthnLogin/Implementations/CreateWebAuthnLoginCredentialCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// 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.Repositories; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs b/src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs index 01914540ac..f313e8995c 100644 --- a/src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs +++ b/src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Identity; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Identity; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs b/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs index cbb91a1e72..381f81dea5 100644 --- a/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs +++ b/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs @@ -1,4 +1,7 @@ -using Duende.IdentityServer.Configuration; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Duende.IdentityServer.Configuration; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Distributed; diff --git a/src/Core/IdentityServer/DistributedCacheCookieManager.cs b/src/Core/IdentityServer/DistributedCacheCookieManager.cs index 5d6717ac41..a01ff63d8f 100644 --- a/src/Core/IdentityServer/DistributedCacheCookieManager.cs +++ b/src/Core/IdentityServer/DistributedCacheCookieManager.cs @@ -1,4 +1,7 @@ -using System.Text; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; diff --git a/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs b/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs index 6a4b7439d4..ad3fdee6f0 100644 --- a/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs +++ b/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Authentication; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Distributed; diff --git a/src/Core/IdentityServer/DistributedCacheTicketStore.cs b/src/Core/IdentityServer/DistributedCacheTicketStore.cs index 949c1173cc..ddf66f04ec 100644 --- a/src/Core/IdentityServer/DistributedCacheTicketStore.cs +++ b/src/Core/IdentityServer/DistributedCacheTicketStore.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Authentication; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Distributed; diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 4965046bfc..cc146800af 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using System.Text; using Bit.Core; using Bit.Core.Auth.Enums; diff --git a/src/Identity/Controllers/SsoController.cs b/src/Identity/Controllers/SsoController.cs index f3dc301a61..edf57a8b5f 100644 --- a/src/Identity/Controllers/SsoController.cs +++ b/src/Identity/Controllers/SsoController.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; diff --git a/src/Identity/IdentityServer/ApiClient.cs b/src/Identity/IdentityServer/ApiClient.cs index 5d768ae806..fa5003e0dc 100644 --- a/src/Identity/IdentityServer/ApiClient.cs +++ b/src/Identity/IdentityServer/ApiClient.cs @@ -1,4 +1,7 @@ -using Bit.Core.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Settings; using Bit.Identity.IdentityServer.RequestValidators; using Duende.IdentityServer.Models; diff --git a/src/Identity/IdentityServer/AuthorizationCodeStore.cs b/src/Identity/IdentityServer/AuthorizationCodeStore.cs index 8215532ba8..17827e818f 100644 --- a/src/Identity/IdentityServer/AuthorizationCodeStore.cs +++ b/src/Identity/IdentityServer/AuthorizationCodeStore.cs @@ -1,4 +1,7 @@ -using Duende.IdentityServer; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Duende.IdentityServer; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; diff --git a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs index a7e2754f00..38945016f3 100644 --- a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.IdentityServer; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.IdentityServer; using Bit.Core.Platform.Installations; using Duende.IdentityServer.Models; using IdentityModel; diff --git a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs index 76842a9e54..e56a135077 100644 --- a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.IdentityServer; using Bit.Core.Repositories; diff --git a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs index dec5f8dc64..0bf28a8258 100644 --- a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs @@ -1,4 +1,7 @@ -using Bit.Core.Identity; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index eb441e7941..a53af41e66 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer; diff --git a/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs index dad9d8e27d..fece7b10b4 100644 --- a/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/IUserDecryptionOptionsBuilder.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Entities; +// 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.Api.Response; using Bit.Core.Entities; diff --git a/src/Identity/IdentityServer/PersistedGrantStore.cs b/src/Identity/IdentityServer/PersistedGrantStore.cs index 70d778430a..b6bdaccc53 100644 --- a/src/Identity/IdentityServer/PersistedGrantStore.cs +++ b/src/Identity/IdentityServer/PersistedGrantStore.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index d7d6708374..c1230f9694 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Identity; diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index dd4592aa0d..0b33dabb77 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index ce5189703e..44dc89d259 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core; using Bit.Core.Auth.Services; diff --git a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index c30c94eeee..fe32d3e1b8 100644 --- a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index e4c1ebd15e..1247feac21 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// 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.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; diff --git a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 76949eb5f7..1e0b3fdfe1 100644 --- a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using System.Text.Json; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; diff --git a/src/Identity/Models/RedirectViewModel.cs b/src/Identity/Models/RedirectViewModel.cs index 5cf7663b4b..99bc67d26b 100644 --- a/src/Identity/Models/RedirectViewModel.cs +++ b/src/Identity/Models/RedirectViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Identity.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Identity.Models; public class RedirectViewModel { diff --git a/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs b/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs index daae846123..a7dba7ce1d 100644 --- a/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs +++ b/src/Identity/Models/Request/Accounts/PreloginRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Identity.Models.Request.Accounts; diff --git a/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs b/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs index 703cb1f350..44f44977dd 100644 --- a/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs +++ b/src/Identity/Models/Request/Accounts/RegisterRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; diff --git a/src/Identity/Program.cs b/src/Identity/Program.cs index 31a69975ad..cb6e7daf39 100644 --- a/src/Identity/Program.cs +++ b/src/Identity/Program.cs @@ -1,4 +1,7 @@ -using AspNetCoreRateLimit; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AspNetCoreRateLimit; using Bit.Core.Utilities; namespace Bit.Identity; diff --git a/src/Infrastructure.Dapper/Auth/Helpers/EmergencyAccessHelpers.cs b/src/Infrastructure.Dapper/Auth/Helpers/EmergencyAccessHelpers.cs index 34e4fcda69..6d35c78a8f 100644 --- a/src/Infrastructure.Dapper/Auth/Helpers/EmergencyAccessHelpers.cs +++ b/src/Infrastructure.Dapper/Auth/Helpers/EmergencyAccessHelpers.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Auth.Entities; namespace Bit.Infrastructure.Dapper.Auth.Helpers; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/AuthRequest.cs b/src/Infrastructure.EntityFramework/Auth/Models/AuthRequest.cs index 0ceccd9f38..2425bdd14a 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/AuthRequest.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/AuthRequest.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.Auth.Models.Data; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/EmergencyAccess.cs b/src/Infrastructure.EntityFramework/Auth/Models/EmergencyAccess.cs index 3ee09e9f83..90e3796bd2 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/EmergencyAccess.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/EmergencyAccess.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.Auth.Models; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs b/src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs index d4fd0c8d36..4fe9be9297 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/SsoConfig.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Auth.Models; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs b/src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs index f8251e9db9..0887e3a121 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/SsoUser.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/WebAuthnCredential.cs b/src/Infrastructure.EntityFramework/Auth/Models/WebAuthnCredential.cs index 696fad7921..aed9a6af19 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/WebAuthnCredential.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/WebAuthnCredential.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.Auth.Models; From fa0c9cb38785a867cc3b12e5678051004ba6362b Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:25:59 -0400 Subject: [PATCH 028/326] Add `#nullable disable` to platform code (#6057) --- .../SecretsManager/Commands/Porting/ImportCommand.cs | 5 ++++- .../Commands/Projects/CreateProjectCommand.cs | 5 ++++- .../ServiceAccounts/CreateServiceAccountCommand.cs | 5 ++++- .../SecretsManager/Queries/AccessClientQuery.cs | 5 ++++- ...eAccountsAccessPoliciesAuthorizationHandlerTests.cs | 2 +- ...eAccountGrantedPoliciesAuthorizationHandlerTests.cs | 2 +- .../Secrets/BulkSecretAuthorizationHandlerTests.cs | 2 +- src/Admin/AdminSettings.cs | 5 ++++- src/Admin/Controllers/HomeController.cs | 5 ++++- .../HostedServices/AzureQueueMailHostedService.cs | 5 ++++- .../IdentityServer/ReadOnlyEnvIdentityUserStore.cs | 5 ++++- src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs | 5 ++++- src/Admin/Jobs/DeleteCiphersJob.cs | 5 ++++- src/Admin/Models/BillingInformationModel.cs | 5 ++++- src/Admin/Models/ChargeBraintreeModel.cs | 5 ++++- src/Admin/Models/CreateUpdateTransactionModel.cs | 5 ++++- src/Admin/Models/CursorPagedModel.cs | 5 ++++- src/Admin/Models/ErrorViewModel.cs | 5 ++++- src/Admin/Models/HomeModel.cs | 5 ++++- src/Admin/Models/PagedModel.cs | 5 ++++- src/Admin/Models/StripeSubscriptionsModel.cs | 5 ++++- src/Admin/Models/UserEditModel.cs | 5 ++++- src/Admin/Models/UserViewModel.cs | 5 ++++- src/Admin/Models/UsersModel.cs | 5 ++++- src/Admin/Services/AccessControlService.cs | 5 ++++- src/Admin/TagHelpers/ActivePageTagHelper.cs | 5 ++++- src/Api/Controllers/CollectionsController.cs | 5 ++++- src/Api/Controllers/DevicesController.cs | 5 ++++- src/Api/Controllers/LicensesController.cs | 5 ++++- .../SelfHostedOrganizationSponsorshipsController.cs | 5 ++++- src/Api/Dirt/Controllers/HibpController.cs | 5 ++++- .../Models/PasswordHealthReportApplicationModel.cs | 5 ++++- src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs | 5 ++++- src/Api/Models/Public/CollectionBaseModel.cs | 5 ++++- .../Public/Request/CollectionUpdateRequestModel.cs | 5 ++++- .../Models/Public/Response/CollectionResponseModel.cs | 5 ++++- src/Api/Models/Public/Response/ErrorResponseModel.cs | 5 ++++- src/Api/Models/Request/Accounts/PremiumRequestModel.cs | 5 ++++- .../Request/Accounts/TaxInfoUpdateRequestModel.cs | 5 ++++- .../Request/Accounts/UpdateAvatarRequestModel.cs | 5 ++++- src/Api/Models/Request/BitPayInvoiceRequestModel.cs | 5 ++++- .../Models/Request/BulkCollectionAccessRequestModel.cs | 5 ++++- src/Api/Models/Request/CollectionRequestModel.cs | 5 ++++- src/Api/Models/Request/DeviceRequestModels.cs | 5 ++++- .../Request/ExpandedTaxInfoUpdateRequestModel.cs | 5 ++++- src/Api/Models/Request/LicenseRequestModel.cs | 5 ++++- .../OrganizationCreateLicenseRequestModel.cs | 5 ++++- .../OrganizationSponsorshipCreateRequestModel.cs | 5 ++++- .../OrganizationUserResetPasswordRequestModel.cs | 5 ++++- src/Api/Models/Request/PaymentRequestModel.cs | 5 ++++- .../Request/SubscriptionCancellationRequestModel.cs | 5 ++++- src/Api/Models/Request/UpdateDomainsRequestModel.cs | 5 ++++- src/Api/Models/Response/CollectionResponseModel.cs | 5 ++++- src/Api/Models/Response/ConfigResponseModel.cs | 5 ++++- src/Api/Models/Response/DeviceResponseModel.cs | 5 ++++- src/Api/Models/Response/DomainsResponseModel.cs | 5 ++++- src/Api/Models/Response/KeysResponseModel.cs | 5 ++++- src/Api/Models/Response/ListResponseModel.cs | 5 ++++- src/Api/Models/Response/PaymentResponseModel.cs | 5 ++++- src/Api/Models/Response/PlanResponseModel.cs | 5 ++++- src/Api/Models/Response/ProfileResponseModel.cs | 5 ++++- src/Api/Models/Response/SubscriptionResponseModel.cs | 5 ++++- src/Api/Models/Response/TaxInfoResponseModel.cs | 5 ++++- .../Installations/Models/InstallationRequestModel.cs | 5 ++++- .../Installations/Models/InstallationResponseModel.cs | 5 ++++- src/Api/Platform/Push/Controllers/PushController.cs | 5 ++++- src/Api/Program.cs | 5 ++++- src/Api/Public/Controllers/CollectionsController.cs | 5 ++++- .../SecretsManager/Controllers/ProjectsController.cs | 5 ++++- .../SecretsManager/Controllers/SecretsController.cs | 5 ++++- .../Controllers/SecretsManagerEventsController.cs | 5 ++++- .../Controllers/SecretsManagerPortingController.cs | 5 ++++- .../Controllers/ServiceAccountsController.cs | 5 ++++- .../Models/Request/AccessTokenCreateRequestModel.cs | 5 ++++- .../Models/Request/GetSecretsRequestModel.cs | 5 ++++- .../Models/Request/PeopleAccessPoliciesRequestModel.cs | 5 ++++- .../Models/Request/ProjectCreateRequestModel.cs | 5 ++++- .../Models/Request/ProjectUpdateRequestModel.cs | 5 ++++- .../Models/Request/RequestSMAccessRequestModel.cs | 5 ++++- .../Models/Request/RevokeAccessTokensRequest.cs | 5 ++++- .../Models/Request/SMImportRequestModel.cs | 5 ++++- .../Models/Request/SecretCreateRequestModel.cs | 5 ++++- .../Models/Request/SecretUpdateRequestModel.cs | 5 ++++- .../Models/Request/SerivceAccountUpdateRequestModel.cs | 5 ++++- .../Models/Request/ServiceAccountCreateRequestModel.cs | 5 ++++- .../Models/Response/AccessTokenResponseModel.cs | 5 ++++- .../Models/Response/BaseSecretResponseModel.cs | 5 ++++- .../Models/Response/PotentialGranteeResponseModel.cs | 5 ++++- .../Models/Response/ProjectResponseModel.cs | 5 ++++- .../Models/Response/SMExportResponseModel.cs | 5 ++++- .../Models/Response/SMImportResponseModel.cs | 5 ++++- .../Response/SecretWithProjectsListResponseModel.cs | 5 ++++- .../Models/Response/ServiceAccountResponseModel.cs | 5 ++++- src/Api/Utilities/ApiExplorerGroupConvention.cs | 5 ++++- src/Api/Utilities/ApiHelpers.cs | 5 ++++- src/Api/Utilities/EnumMatchesAttribute.cs | 5 ++++- src/Api/Utilities/ExceptionHandlerFilterAttribute.cs | 5 ++++- src/Api/Utilities/MultipartFormDataHelper.cs | 5 ++++- .../Utilities/PublicApiControllersModelConvention.cs | 5 ++++- src/Core/Constants.cs | 5 ++++- src/Core/Context/CurrentContext.cs | 5 ++++- .../OrganizationSponsorshipRequestModel.cs | 5 ++++- .../OrganizationSponsorshipSyncRequestModel.cs | 5 ++++- src/Core/Models/Api/Request/PushDeviceRequestModel.cs | 5 ++++- .../Models/Api/Request/PushRegistrationRequestModel.cs | 5 ++++- src/Core/Models/Api/Request/PushUpdateRequestModel.cs | 5 ++++- src/Core/Models/Api/Response/Duo/DuoResponseModel.cs | 5 ++++- src/Core/Models/Api/Response/ErrorResponseModel.cs | 5 ++++- .../OrganizationSponsorshipResponseModel.cs | 5 ++++- .../OrganizationSponsorshipSyncResponseModel.cs | 5 ++++- src/Core/Models/Business/CompleteSubscriptionUpdate.cs | 5 ++++- src/Core/Models/Business/OrganizationSignup.cs | 5 ++++- src/Core/Models/Business/OrganizationUpgrade.cs | 5 ++++- src/Core/Models/Business/SubscriptionInfo.cs | 5 ++++- src/Core/Models/Business/SubscriptionUpdate.cs | 5 ++++- src/Core/Models/Business/TaxInfo.cs | 5 ++++- .../OrganizationSponsorshipOfferTokenable.cs | 5 ++++- src/Core/Models/Business/UserLicense.cs | 5 ++++- src/Core/Models/Data/CollectionAccessDetails.cs | 5 ++++- src/Core/Models/Data/InstallationDeviceEntity.cs | 5 ++++- .../OrganizationConnectionData.cs | 5 ++++- .../Organizations/OrganizationDomainSsoDetailsData.cs | 5 ++++- .../OrganizationSponsorshipData.cs | 5 ++++- .../OrganizationSponsorshipSyncData.cs | 5 ++++- .../VerifiedOrganizationDomainSsoDetail.cs | 5 ++++- src/Core/Models/Data/PageOptions.cs | 5 ++++- src/Core/Models/Data/PagedResult.cs | 5 ++++- src/Core/Models/IExternal.cs | 5 ++++- src/Core/Models/Mail/AdminResetPasswordViewModel.cs | 5 ++++- src/Core/Models/Mail/BaseMailModel.cs | 5 ++++- src/Core/Models/Mail/BaseTitleContactUsMailModel.cs | 5 ++++- src/Core/Models/Mail/ChangeEmailExistsViewModel.cs | 5 ++++- .../Mail/ClaimedDomainUserNotificationViewModel.cs | 5 ++++- .../FamiliesForEnterpriseOfferViewModel.cs | 5 ++++- .../FamiliesForEnterpriseRemoveOfferViewModel.cs | 5 ++++- src/Core/Models/Mail/InvoiceUpcomingViewModel.cs | 5 ++++- src/Core/Models/Mail/LicenseExpiredViewModel.cs | 5 ++++- src/Core/Models/Mail/MailMessage.cs | 5 ++++- src/Core/Models/Mail/MailQueueMessage.cs | 5 ++++- src/Core/Models/Mail/NewDeviceLoggedInModel.cs | 5 ++++- .../Mail/OrganizationDomainUnverifiedViewModel.cs | 5 ++++- .../Models/Mail/OrganizationInitiateDeleteModel.cs | 5 ++++- src/Core/Models/Mail/OrganizationInvitesInfo.cs | 5 ++++- .../Mail/OrganizationSeatsAutoscaledViewModel.cs | 5 ++++- .../Mail/OrganizationSeatsMaxReachedViewModel.cs | 5 ++++- .../OrganizationServiceAccountsMaxReachedViewModel.cs | 5 ++++- .../Models/Mail/OrganizationUserAcceptedViewModel.cs | 5 ++++- .../Models/Mail/OrganizationUserConfirmedViewModel.cs | 5 ++++- .../Models/Mail/OrganizationUserInvitedViewModel.cs | 5 ++++- ...ganizationUserRemovedForPolicySingleOrgViewModel.cs | 5 ++++- ...OrganizationUserRemovedForPolicyTwoStepViewModel.cs | 5 ++++- ...ganizationUserRevokedForPolicySingleOrgViewModel.cs | 5 ++++- ...ganizationUserRevokedForPolicyTwoFactorViewModel.cs | 5 ++++- .../Mail/Provider/ProviderInitiateDeleteModel.cs | 5 ++++- .../Mail/Provider/ProviderSetupInviteViewModel.cs | 5 ++++- .../Provider/ProviderUpdatePaymentMethodViewModel.cs | 5 ++++- .../Mail/Provider/ProviderUserConfirmedViewModel.cs | 5 ++++- .../Mail/Provider/ProviderUserInvitedViewModel.cs | 5 ++++- .../Mail/Provider/ProviderUserRemovedViewModel.cs | 5 ++++- .../Models/Mail/SecurityTaskNotificationViewModel.cs | 5 ++++- src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs | 5 ++++- src/Core/Models/Mail/UpdateTempPasswordViewModel.cs | 5 ++++- .../Models/Mail/UserVerificationEmailTokenViewModel.cs | 5 ++++- .../OrganizationConnectionConfigs/BillingSyncConfig.cs | 5 ++++- .../Models/Stripe/StripeSubscriptionListOptions.cs | 5 ++++- .../OrganizationCollections/CreateCollectionCommand.cs | 5 ++++- .../Interfaces/ICreateCollectionCommand.cs | 5 ++++- .../Interfaces/IUpdateCollectionCommand.cs | 5 ++++- .../OrganizationCollections/UpdateCollectionCommand.cs | 5 ++++- .../FamiliesForEnterprise/CancelSponsorshipCommand.cs | 5 ++++- .../Cloud/SendSponsorshipOfferCommand.cs | 5 ++++- .../Cloud/ValidateRedemptionTokenCommand.cs | 5 ++++- .../Cloud/ValidateSponsorshipCommand.cs | 5 ++++- .../SelfHosted/SelfHostedSyncSponsorshipsCommand.cs | 5 ++++- .../UpdateSecretsManagerSubscriptionCommand.cs | 5 ++++- .../UpgradeOrganizationPlanCommand.cs | 5 ++++- .../AzurePhishingDomainStorageService.cs | 5 ++++- .../CloudPhishingDomainRelayQuery.cs | 5 ++++- src/Core/SecretsManager/Commands/Porting/SMImport.cs | 5 ++++- .../Models/Data/ApiKeyClientSecretDetails.cs | 5 ++++- src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs | 5 ++++- src/Core/SecretsManager/Models/Data/PeopleGrantees.cs | 5 ++++- .../Models/Data/ProjectPeopleAccessPolicies.cs | 5 ++++- .../Models/Data/ProjectPermissionDetails.cs | 5 ++++- .../Models/Data/SecretPermissionDetails.cs | 5 ++++- .../Models/Data/ServiceAccountPeopleAccessPolicies.cs | 5 ++++- .../Models/Data/ServiceAccountSecretsDetails.cs | 5 ++++- .../Mail/RequestSecretsManagerAccessViewModel.cs | 5 ++++- .../SecretsManager/Repositories/IProjectRepository.cs | 5 ++++- .../SecretsManager/Repositories/ISecretRepository.cs | 5 ++++- .../Repositories/IServiceAccountRepository.cs | 5 ++++- .../Repositories/Noop/NoopProjectRepository.cs | 5 ++++- .../Repositories/Noop/NoopSecretRepository.cs | 5 ++++- .../Repositories/Noop/NoopServiceAccountRepository.cs | 5 ++++- src/Core/Services/IFeatureService.cs | 5 ++++- src/Core/Services/II18nService.cs | 10 ++++++---- src/Core/Services/IPaymentService.cs | 5 ++++- src/Core/Services/IStripeAdapter.cs | 5 ++++- src/Core/Services/IUserService.cs | 5 ++++- .../Implementations/BaseIdentityClientService.cs | 5 ++++- src/Core/Services/Implementations/I18nService.cs | 8 +++++--- .../Implementations/InMemoryApplicationCacheService.cs | 5 ++++- .../Implementations/LaunchDarklyFeatureService.cs | 5 ++++- src/Core/Services/Implementations/LicensingService.cs | 5 ++++- .../Implementations/MailKitSmtpMailDeliveryService.cs | 5 ++++- src/Core/Services/Implementations/StripeAdapter.cs | 5 ++++- .../Services/Implementations/StripePaymentService.cs | 5 ++++- src/Core/Services/Implementations/UserService.cs | 5 ++++- src/Core/Settings/GlobalSettings.cs | 5 ++++- src/Core/Tokens/DataProtectorTokenFactory.cs | 5 ++++- src/Core/Tokens/Tokenable.cs | 5 ++++- src/Core/Utilities/AssemblyHelpers.cs | 5 ++++- src/Core/Utilities/BitPayClient.cs | 5 ++++- src/Core/Utilities/BulkAuthorizationHandler.cs | 5 ++++- src/Core/Utilities/CustomRedisProcessingStrategy.cs | 5 ++++- src/Core/Utilities/DistributedCacheExtensions.cs | 5 ++++- src/Core/Utilities/HandlebarsObjectJsonConverter.cs | 5 ++++- src/Core/Utilities/JsonHelpers.cs | 5 ++++- src/Core/Utilities/LoggerFactoryExtensions.cs | 5 ++++- src/Core/Utilities/StaticStore.cs | 5 ++++- src/Core/Utilities/StrictEmailAddressAttribute.cs | 5 ++++- src/Core/Utilities/StrictEmailAddressListAttribute.cs | 5 ++++- src/Core/Utilities/SystemTextJsonCosmosSerializer.cs | 5 ++++- src/Icons/Controllers/IconsController.cs | 5 ++++- src/Icons/Models/DomainName.cs | 5 ++++- src/Icons/Models/Icon.cs | 5 ++++- .../SecretsManager/Repositories/ApiKeyRepository.cs | 5 ++++- .../Converters/DataProtectionConverter.cs | 5 ++++- .../Dirt/Models/OrganizationApplication.cs | 5 ++++- .../Dirt/Models/OrganizationReport.cs | 5 ++++- .../Dirt/Models/PasswordHealthReportApplication.cs | 5 ++++- .../Dirt/Repositories/OrganizationReportRepository.cs | 5 ++++- .../Models/Collection.cs | 5 ++++- .../Models/CollectionCipher.cs | 5 ++++- .../Models/CollectionGroup.cs | 5 ++++- .../Models/CollectionUser.cs | 5 ++++- src/Infrastructure.EntityFramework/Models/Device.cs | 5 ++++- src/Infrastructure.EntityFramework/Models/Group.cs | 5 ++++- src/Infrastructure.EntityFramework/Models/GroupUser.cs | 5 ++++- .../Models/OrganizationApiKey.cs | 5 ++++- .../Models/OrganizationConnection.cs | 5 ++++- .../Models/OrganizationDomain.cs | 5 ++++- .../Models/OrganizationSponsorship.cs | 5 ++++- .../Models/OrganizationUser.cs | 5 ++++- .../Models/Transaction.cs | 5 ++++- src/Infrastructure.EntityFramework/Models/User.cs | 5 ++++- .../NotificationCenter/Models/Notification.cs | 5 ++++- .../NotificationCenter/Models/NotificationStatus.cs | 5 ++++- .../Repositories/Queries/UserCipherDetailsQuery.cs | 5 ++++- .../SecretsManager/Models/AccessPolicy.cs | 5 ++++- .../SecretsManager/Models/ApiKey.cs | 5 ++++- .../SecretsManager/Models/Project.cs | 5 ++++- .../SecretsManager/Models/Secret.cs | 5 ++++- .../SecretsManager/Models/ServiceAccount.cs | 5 ++++- src/Notifications/AnonymousNotificationsHub.cs | 5 ++++- src/Notifications/AzureQueueHostedService.cs | 5 ++++- src/Notifications/HeartbeatHostedService.cs | 5 ++++- src/Notifications/HubHelpers.cs | 5 ++++- src/Notifications/NotificationsHub.cs | 5 ++++- src/Notifications/SubjectUserIdProvider.cs | 5 ++++- src/SharedWeb/Health/HealthCheckServiceExtensions.cs | 5 ++++- src/SharedWeb/Utilities/DisplayAttributeHelpers.cs | 5 ++++- .../Utilities/ExceptionHandlerFilterAttribute.cs | 5 ++++- src/SharedWeb/Utilities/ServiceCollectionExtensions.cs | 5 ++++- .../Attributes/BitMemberAutoDataAttribute.cs | 5 ++++- .../AutoFixture/Attributes/CustomAutoDataAttribute.cs | 5 ++++- .../AutoFixture/Attributes/EnvironmentDataAttribute.cs | 5 ++++- .../Attributes/JsonDocumentCustomizeAttribute.cs | 5 ++++- .../Common/AutoFixture/BuilderWithoutAutoProperties.cs | 5 ++++- test/Common/AutoFixture/JsonDocumentFixtures.cs | 5 ++++- test/Common/AutoFixture/SutProvider.cs | 5 ++++- test/Common/AutoFixture/SutProviderCustomization.cs | 5 ++++- test/Common/AutoFixture/SutProviderExtensions.cs | 5 ++++- test/Common/Fakes/FakeDataProtectorTokenFactory.cs | 5 ++++- test/Common/Helpers/AssertHelper.cs | 5 ++++- test/Common/MockedHttpClient/HttpResponseBuilder.cs | 5 ++++- test/Common/MockedHttpClient/MockedHttpResponse.cs | 5 ++++- .../Factories/IdentityApplicationFactory.cs | 5 ++++- .../Factories/WebApplicationFactoryExtensions.cs | 5 ++++- .../FakeRemoteIpAddressMiddleware.cs | 5 ++++- test/IntegrationTestCommon/SqlServerTestDatabase.cs | 5 ++++- util/Migrator/DbMigrator.cs | 5 ++++- util/Server/Program.cs | 5 ++++- util/Setup/AppIdBuilder.cs | 5 ++++- util/Setup/Configuration.cs | 5 ++++- util/Setup/Context.cs | 5 ++++- util/Setup/EnvironmentFileBuilder.cs | 5 ++++- util/Setup/Helpers.cs | 5 ++++- util/Setup/NginxConfigBuilder.cs | 5 ++++- util/Setup/Program.cs | 5 ++++- util/Setup/YamlComments.cs | 5 ++++- 291 files changed, 1158 insertions(+), 296 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Porting/ImportCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Porting/ImportCommand.cs index 9520f6f00f..dc389256a1 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Porting/ImportCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Porting/ImportCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Commands.Porting; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Commands.Porting; using Bit.Core.SecretsManager.Commands.Porting.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs index d54644e292..1a5fe07c21 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Context; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Identity; using Bit.Core.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs index 687291d75a..12c7f679bd 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Repositories; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Repositories; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessClientQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessClientQuery.cs index 8847ee293f..87548e5b6c 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessClientQuery.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/AccessClientQuery.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.SecretsManager.Queries.Interfaces; diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs index 4f87396824..17c92443cc 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests.cs @@ -295,7 +295,7 @@ public class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests { sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) .Returns(true); - sutProvider.GetDependency().GetAccessClientAsync(default, resource.OrganizationId) + sutProvider.GetDependency().GetAccessClientAsync(default!, resource.OrganizationId) .ReturnsForAnyArgs((accessClientType, userId)); } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs index 6f36684c44..45fe8c588f 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/AccessPolicies/ServiceAccountGrantedPoliciesAuthorizationHandlerTests.cs @@ -247,7 +247,7 @@ public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests { sutProvider.GetDependency().AccessSecretsManager(resource.OrganizationId) .Returns(true); - sutProvider.GetDependency().GetAccessClientAsync(default, resource.OrganizationId) + sutProvider.GetDependency().GetAccessClientAsync(default!, resource.OrganizationId) .ReturnsForAnyArgs((accessClientType, userId)); } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandlerTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandlerTests.cs index d7dc11ba70..a015b1a02a 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandlerTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AuthorizationHandlers/Secrets/BulkSecretAuthorizationHandlerTests.cs @@ -207,7 +207,7 @@ public class BulkSecretAuthorizationHandlerTests { sutProvider.GetDependency().AccessSecretsManager(organizationId) .Returns(true); - sutProvider.GetDependency().GetAccessClientAsync(default, organizationId) + sutProvider.GetDependency().GetAccessClientAsync(default!, organizationId) .ReturnsForAnyArgs((accessClientType, userId)); } diff --git a/src/Admin/AdminSettings.cs b/src/Admin/AdminSettings.cs index 18694e3e38..0ecae5c82e 100644 --- a/src/Admin/AdminSettings.cs +++ b/src/Admin/AdminSettings.cs @@ -1,4 +1,7 @@ -namespace Bit.Admin; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Admin; public class AdminSettings { diff --git a/src/Admin/Controllers/HomeController.cs b/src/Admin/Controllers/HomeController.cs index 20c1be70d0..debe5979f5 100644 --- a/src/Admin/Controllers/HomeController.cs +++ b/src/Admin/Controllers/HomeController.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using System.Text.Json; using Bit.Admin.Models; using Bit.Core.Settings; diff --git a/src/Admin/HostedServices/AzureQueueMailHostedService.cs b/src/Admin/HostedServices/AzureQueueMailHostedService.cs index cff724e4f3..4669b2b2ec 100644 --- a/src/Admin/HostedServices/AzureQueueMailHostedService.cs +++ b/src/Admin/HostedServices/AzureQueueMailHostedService.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Storage.Queues; using Azure.Storage.Queues.Models; using Bit.Core.Models.Mail; diff --git a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs index 89f04230b3..ba5c6c0cfd 100644 --- a/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs +++ b/src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs @@ -1,4 +1,7 @@ -using Bit.Core.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; namespace Bit.Admin.IdentityServer; diff --git a/src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs b/src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs index 88f3a40b1a..4a81745241 100644 --- a/src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs +++ b/src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Identity; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Identity; namespace Bit.Admin.IdentityServer; diff --git a/src/Admin/Jobs/DeleteCiphersJob.cs b/src/Admin/Jobs/DeleteCiphersJob.cs index ee48a26d16..b1fc9c53c6 100644 --- a/src/Admin/Jobs/DeleteCiphersJob.cs +++ b/src/Admin/Jobs/DeleteCiphersJob.cs @@ -1,4 +1,7 @@ -using Bit.Core; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core; using Bit.Core.Jobs; using Bit.Core.Vault.Repositories; using Microsoft.Extensions.Options; diff --git a/src/Admin/Models/BillingInformationModel.cs b/src/Admin/Models/BillingInformationModel.cs index ecc06919fa..c6c7ce82c9 100644 --- a/src/Admin/Models/BillingInformationModel.cs +++ b/src/Admin/Models/BillingInformationModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; namespace Bit.Admin.Models; diff --git a/src/Admin/Models/ChargeBraintreeModel.cs b/src/Admin/Models/ChargeBraintreeModel.cs index 8c2f39e58d..195c0a1f0c 100644 --- a/src/Admin/Models/ChargeBraintreeModel.cs +++ b/src/Admin/Models/ChargeBraintreeModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Admin.Models; diff --git a/src/Admin/Models/CreateUpdateTransactionModel.cs b/src/Admin/Models/CreateUpdateTransactionModel.cs index 8004546f9e..41b7a30413 100644 --- a/src/Admin/Models/CreateUpdateTransactionModel.cs +++ b/src/Admin/Models/CreateUpdateTransactionModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Admin/Models/CursorPagedModel.cs b/src/Admin/Models/CursorPagedModel.cs index 35a4de922a..b6475ad220 100644 --- a/src/Admin/Models/CursorPagedModel.cs +++ b/src/Admin/Models/CursorPagedModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Admin.Models; public class CursorPagedModel { diff --git a/src/Admin/Models/ErrorViewModel.cs b/src/Admin/Models/ErrorViewModel.cs index 3b24a1ece7..dc39c2f004 100644 --- a/src/Admin/Models/ErrorViewModel.cs +++ b/src/Admin/Models/ErrorViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Admin.Models; public class ErrorViewModel { diff --git a/src/Admin/Models/HomeModel.cs b/src/Admin/Models/HomeModel.cs index 900a04e41a..f4006d6c30 100644 --- a/src/Admin/Models/HomeModel.cs +++ b/src/Admin/Models/HomeModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Settings; namespace Bit.Admin.Models; diff --git a/src/Admin/Models/PagedModel.cs b/src/Admin/Models/PagedModel.cs index 4c9c8e1713..3fec874ae5 100644 --- a/src/Admin/Models/PagedModel.cs +++ b/src/Admin/Models/PagedModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Admin.Models; public abstract class PagedModel { diff --git a/src/Admin/Models/StripeSubscriptionsModel.cs b/src/Admin/Models/StripeSubscriptionsModel.cs index 99e9c5b77a..36e1f099e1 100644 --- a/src/Admin/Models/StripeSubscriptionsModel.cs +++ b/src/Admin/Models/StripeSubscriptionsModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// 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; diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index 2597da6e96..cfbb05a5ac 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Settings; diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 7fddbc0f54..719ad7813c 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Vault.Entities; diff --git a/src/Admin/Models/UsersModel.cs b/src/Admin/Models/UsersModel.cs index 33148301b2..191a34547d 100644 --- a/src/Admin/Models/UsersModel.cs +++ b/src/Admin/Models/UsersModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Admin.Models; public class UsersModel : PagedModel { diff --git a/src/Admin/Services/AccessControlService.cs b/src/Admin/Services/AccessControlService.cs index a2ba9fa6ff..f512ec7494 100644 --- a/src/Admin/Services/AccessControlService.cs +++ b/src/Admin/Services/AccessControlService.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Admin.Enums; using Bit.Admin.Utilities; using Bit.Core.Settings; diff --git a/src/Admin/TagHelpers/ActivePageTagHelper.cs b/src/Admin/TagHelpers/ActivePageTagHelper.cs index a148e3cdf7..bc8e9afafb 100644 --- a/src/Admin/TagHelpers/ActivePageTagHelper.cs +++ b/src/Admin/TagHelpers/ActivePageTagHelper.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc.Controllers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 87f53b0891..30d007a57e 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.Context; diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index eaa572b7ec..07e8552268 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Auth.Models.Request; using Bit.Api.Models.Request; using Bit.Api.Models.Response; diff --git a/src/Api/Controllers/LicensesController.cs b/src/Api/Controllers/LicensesController.cs index 1c00589201..e735cf3b4b 100644 --- a/src/Api/Controllers/LicensesController.cs +++ b/src/Api/Controllers/LicensesController.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api.OrganizationLicenses; diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index 371b321a4c..de41a4cf10 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Authorization.Requirements; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core.Context; diff --git a/src/Api/Dirt/Controllers/HibpController.cs b/src/Api/Dirt/Controllers/HibpController.cs index e0ec40d0ab..d108fdbd4f 100644 --- a/src/Api/Dirt/Controllers/HibpController.cs +++ b/src/Api/Dirt/Controllers/HibpController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using System.Security.Cryptography; using Bit.Core.Context; using Bit.Core.Exceptions; diff --git a/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs b/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs index 5dbc07afb5..0a57f0117e 100644 --- a/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs +++ b/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.Dirt.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.Dirt.Models; public class PasswordHealthReportApplicationModel { diff --git a/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs index b4d9d75aa0..7fa9ff068e 100644 --- a/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs +++ b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Jobs; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; diff --git a/src/Api/Models/Public/CollectionBaseModel.cs b/src/Api/Models/Public/CollectionBaseModel.cs index 0dd4b6ce85..aff5485c31 100644 --- a/src/Api/Models/Public/CollectionBaseModel.cs +++ b/src/Api/Models/Public/CollectionBaseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Models.Public; diff --git a/src/Api/Models/Public/Request/CollectionUpdateRequestModel.cs b/src/Api/Models/Public/Request/CollectionUpdateRequestModel.cs index 0adc6afa77..fa8432fa04 100644 --- a/src/Api/Models/Public/Request/CollectionUpdateRequestModel.cs +++ b/src/Api/Models/Public/Request/CollectionUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Public.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Core.Entities; namespace Bit.Api.Models.Public.Request; diff --git a/src/Api/Models/Public/Response/CollectionResponseModel.cs b/src/Api/Models/Public/Response/CollectionResponseModel.cs index d08db64290..2d13f982cd 100644 --- a/src/Api/Models/Public/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Public/Response/CollectionResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/Models/Public/Response/ErrorResponseModel.cs b/src/Api/Models/Public/Response/ErrorResponseModel.cs index 4a4887a0e7..c5bb06d02e 100644 --- a/src/Api/Models/Public/Response/ErrorResponseModel.cs +++ b/src/Api/Models/Public/Response/ErrorResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Api.Models.Public.Response; diff --git a/src/Api/Models/Request/Accounts/PremiumRequestModel.cs b/src/Api/Models/Request/Accounts/PremiumRequestModel.cs index 26d199381f..4e9882d67c 100644 --- a/src/Api/Models/Request/Accounts/PremiumRequestModel.cs +++ b/src/Api/Models/Request/Accounts/PremiumRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Settings; using Enums = Bit.Core.Enums; diff --git a/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs b/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs index f51580408a..5f58453a6d 100644 --- a/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs +++ b/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Models.Request.Accounts; diff --git a/src/Api/Models/Request/Accounts/UpdateAvatarRequestModel.cs b/src/Api/Models/Request/Accounts/UpdateAvatarRequestModel.cs index 2dd7b27945..225bccc4bf 100644 --- a/src/Api/Models/Request/Accounts/UpdateAvatarRequestModel.cs +++ b/src/Api/Models/Request/Accounts/UpdateAvatarRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; namespace Bit.Api.Models.Request.Accounts; diff --git a/src/Api/Models/Request/BitPayInvoiceRequestModel.cs b/src/Api/Models/Request/BitPayInvoiceRequestModel.cs index 66a5931ca0..d27736d712 100644 --- a/src/Api/Models/Request/BitPayInvoiceRequestModel.cs +++ b/src/Api/Models/Request/BitPayInvoiceRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Settings; namespace Bit.Api.Models.Request; diff --git a/src/Api/Models/Request/BulkCollectionAccessRequestModel.cs b/src/Api/Models/Request/BulkCollectionAccessRequestModel.cs index 8076d8ea5a..f0874cf987 100644 --- a/src/Api/Models/Request/BulkCollectionAccessRequestModel.cs +++ b/src/Api/Models/Request/BulkCollectionAccessRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.Models.Request; public class BulkCollectionAccessRequestModel { diff --git a/src/Api/Models/Request/CollectionRequestModel.cs b/src/Api/Models/Request/CollectionRequestModel.cs index 59fa0160a3..9aa80b859b 100644 --- a/src/Api/Models/Request/CollectionRequestModel.cs +++ b/src/Api/Models/Request/CollectionRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Api/Models/Request/DeviceRequestModels.cs b/src/Api/Models/Request/DeviceRequestModels.cs index 99465501d9..397d4e27df 100644 --- a/src/Api/Models/Request/DeviceRequestModels.cs +++ b/src/Api/Models/Request/DeviceRequestModels.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.NotificationHub; diff --git a/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs b/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs index 7f95d755a5..5b526360f9 100644 --- a/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs +++ b/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Request.Accounts; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Request.Accounts; namespace Bit.Api.Models.Request; diff --git a/src/Api/Models/Request/LicenseRequestModel.cs b/src/Api/Models/Request/LicenseRequestModel.cs index 7b66d95f0e..8851f71eaa 100644 --- a/src/Api/Models/Request/LicenseRequestModel.cs +++ b/src/Api/Models/Request/LicenseRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Models.Request; diff --git a/src/Api/Models/Request/Organizations/OrganizationCreateLicenseRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCreateLicenseRequestModel.cs index 5ee7a632a6..13e0371c51 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCreateLicenseRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCreateLicenseRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Core.Utilities; diff --git a/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs index 896b5799e0..0dd2e892ac 100644 --- a/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Enums; using Bit.Core.Utilities; diff --git a/src/Api/Models/Request/Organizations/OrganizationUserResetPasswordRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationUserResetPasswordRequestModel.cs index 571f69c1ef..1278cd5b53 100644 --- a/src/Api/Models/Request/Organizations/OrganizationUserResetPasswordRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationUserResetPasswordRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Models.Request.Organizations; diff --git a/src/Api/Models/Request/PaymentRequestModel.cs b/src/Api/Models/Request/PaymentRequestModel.cs index eae1abfce2..4bc4a4d02b 100644 --- a/src/Api/Models/Request/PaymentRequestModel.cs +++ b/src/Api/Models/Request/PaymentRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Enums; namespace Bit.Api.Models.Request; diff --git a/src/Api/Models/Request/SubscriptionCancellationRequestModel.cs b/src/Api/Models/Request/SubscriptionCancellationRequestModel.cs index 318c40aa21..8630398e52 100644 --- a/src/Api/Models/Request/SubscriptionCancellationRequestModel.cs +++ b/src/Api/Models/Request/SubscriptionCancellationRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.Models.Request; public class SubscriptionCancellationRequestModel { diff --git a/src/Api/Models/Request/UpdateDomainsRequestModel.cs b/src/Api/Models/Request/UpdateDomainsRequestModel.cs index 47c5d05dec..af53967267 100644 --- a/src/Api/Models/Request/UpdateDomainsRequestModel.cs +++ b/src/Api/Models/Request/UpdateDomainsRequestModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/Models/Response/CollectionResponseModel.cs b/src/Api/Models/Response/CollectionResponseModel.cs index 5ce8310117..5eb543e864 100644 --- a/src/Api/Models/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Response/CollectionResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Data; diff --git a/src/Api/Models/Response/ConfigResponseModel.cs b/src/Api/Models/Response/ConfigResponseModel.cs index 4571089295..d748254206 100644 --- a/src/Api/Models/Response/ConfigResponseModel.cs +++ b/src/Api/Models/Response/ConfigResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Services; diff --git a/src/Api/Models/Response/DeviceResponseModel.cs b/src/Api/Models/Response/DeviceResponseModel.cs index 44f8a16db2..4acaeea793 100644 --- a/src/Api/Models/Response/DeviceResponseModel.cs +++ b/src/Api/Models/Response/DeviceResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Api/Models/Response/DomainsResponseModel.cs b/src/Api/Models/Response/DomainsResponseModel.cs index 5b6b4e59c8..4df161f38e 100644 --- a/src/Api/Models/Response/DomainsResponseModel.cs +++ b/src/Api/Models/Response/DomainsResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Api/Models/Response/KeysResponseModel.cs b/src/Api/Models/Response/KeysResponseModel.cs index 2f7e5e7304..cfc1a6a0a1 100644 --- a/src/Api/Models/Response/KeysResponseModel.cs +++ b/src/Api/Models/Response/KeysResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; diff --git a/src/Api/Models/Response/ListResponseModel.cs b/src/Api/Models/Response/ListResponseModel.cs index ecfe0a7e19..746e6c197b 100644 --- a/src/Api/Models/Response/ListResponseModel.cs +++ b/src/Api/Models/Response/ListResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; diff --git a/src/Api/Models/Response/PaymentResponseModel.cs b/src/Api/Models/Response/PaymentResponseModel.cs index 067ac969ec..1effe8bb1d 100644 --- a/src/Api/Models/Response/PaymentResponseModel.cs +++ b/src/Api/Models/Response/PaymentResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; diff --git a/src/Api/Models/Response/PlanResponseModel.cs b/src/Api/Models/Response/PlanResponseModel.cs index f48a06b4ec..6f2f752803 100644 --- a/src/Api/Models/Response/PlanResponseModel.cs +++ b/src/Api/Models/Response/PlanResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Models.Api; diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index 246b3c3227..cbdfaf0f16 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Entities; diff --git a/src/Api/Models/Response/SubscriptionResponseModel.cs b/src/Api/Models/Response/SubscriptionResponseModel.cs index c7aae1dec2..b460877d30 100644 --- a/src/Api/Models/Response/SubscriptionResponseModel.cs +++ b/src/Api/Models/Response/SubscriptionResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Utilities; diff --git a/src/Api/Models/Response/TaxInfoResponseModel.cs b/src/Api/Models/Response/TaxInfoResponseModel.cs index c1cd51267e..67896abac6 100644 --- a/src/Api/Models/Response/TaxInfoResponseModel.cs +++ b/src/Api/Models/Response/TaxInfoResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Business; namespace Bit.Api.Models.Response; diff --git a/src/Api/Platform/Installations/Models/InstallationRequestModel.cs b/src/Api/Platform/Installations/Models/InstallationRequestModel.cs index 242701a66f..2237eedf92 100644 --- a/src/Api/Platform/Installations/Models/InstallationRequestModel.cs +++ b/src/Api/Platform/Installations/Models/InstallationRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Platform.Installations; using Bit.Core.Utilities; diff --git a/src/Api/Platform/Installations/Models/InstallationResponseModel.cs b/src/Api/Platform/Installations/Models/InstallationResponseModel.cs index 0be5795275..c48a453426 100644 --- a/src/Api/Platform/Installations/Models/InstallationResponseModel.cs +++ b/src/Api/Platform/Installations/Models/InstallationResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.Platform.Installations; namespace Bit.Api.Platform.Installations; diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index af24a7b2ca..88aec18be3 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using System.Text.Json; using Bit.Core.Context; using Bit.Core.Exceptions; diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 2fd25eaefa..6023f51c6d 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -1,4 +1,7 @@ -using AspNetCoreRateLimit; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AspNetCoreRateLimit; using Bit.Core.Utilities; using Microsoft.IdentityModel.Tokens; diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index ec282a0e4d..656f2980ca 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.Models.Public.Request; using Bit.Api.Models.Public.Response; using Bit.Core.Context; diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index a6929bc193..0af122fa57 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 519bc328fa..e32d5cd581 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs index 91d350b680..af162fe399 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerEventsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs index 7599bd262b..7468586702 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs @@ -1,4 +1,7 @@ -using Bit.Api.SecretsManager.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; using Bit.Core.Enums; diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 96c6c60528..499c496cc9 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Billing.Pricing; diff --git a/src/Api/SecretsManager/Models/Request/AccessTokenCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/AccessTokenCreateRequestModel.cs index 2d961ad824..20014b6730 100644 --- a/src/Api/SecretsManager/Models/Request/AccessTokenCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/AccessTokenCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs b/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs index 5eec3a7a6c..84238ae149 100644 --- a/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/GetSecretsRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.SecretsManager.Models.Request; public class GetSecretsRequestModel : IValidatableObject diff --git a/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs index 1ce74aca3c..d6f1396ed5 100644 --- a/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/PeopleAccessPoliciesRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.SecretsManager.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.SecretsManager.Utilities; using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs index 3014ecdf82..73b8f0cdc9 100644 --- a/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ProjectCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs index 176b6cc598..a582e87d75 100644 --- a/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ProjectUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs b/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs index 1f05bad933..b3a9e2a140 100644 --- a/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/RequestSMAccessRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.SecretsManager.Models.Request; diff --git a/src/Api/SecretsManager/Models/Request/RevokeAccessTokensRequest.cs b/src/Api/SecretsManager/Models/Request/RevokeAccessTokensRequest.cs index ecced7a5cd..5dcce209fc 100644 --- a/src/Api/SecretsManager/Models/Request/RevokeAccessTokensRequest.cs +++ b/src/Api/SecretsManager/Models/Request/RevokeAccessTokensRequest.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; public class RevokeAccessTokensRequest { diff --git a/src/Api/SecretsManager/Models/Request/SMImportRequestModel.cs b/src/Api/SecretsManager/Models/Request/SMImportRequestModel.cs index a63e2c180d..a9ee6023bc 100644 --- a/src/Api/SecretsManager/Models/Request/SMImportRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SMImportRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Commands.Porting; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs index 6c0d41c2dd..20cdcf005d 100644 --- a/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs index 7d298bfa0f..b95bc9e500 100644 --- a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs index 017749725f..1c50ac059c 100644 --- a/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SerivceAccountUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs b/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs index 6771669209..ba27189281 100644 --- a/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/ServiceAccountCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.SecretsManager.Entities; using Bit.Core.Utilities; diff --git a/src/Api/SecretsManager/Models/Response/AccessTokenResponseModel.cs b/src/Api/SecretsManager/Models/Response/AccessTokenResponseModel.cs index 72f9fcac64..50fee5f976 100644 --- a/src/Api/SecretsManager/Models/Response/AccessTokenResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/AccessTokenResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; namespace Bit.Api.SecretsManager.Models.Response; diff --git a/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs b/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs index 0579baec07..26425b53d0 100644 --- a/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/BaseSecretResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; namespace Bit.Api.SecretsManager.Models.Response; diff --git a/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs index 002ba1525b..9bc274430d 100644 --- a/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/PotentialGranteeResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Api/SecretsManager/Models/Response/ProjectResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectResponseModel.cs index c2a1b9a09f..f7b0bb5c9c 100644 --- a/src/Api/SecretsManager/Models/Response/ProjectResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ProjectResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Api/SecretsManager/Models/Response/SMExportResponseModel.cs b/src/Api/SecretsManager/Models/Response/SMExportResponseModel.cs index 6d83117c32..c361e8abc3 100644 --- a/src/Api/SecretsManager/Models/Response/SMExportResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SMExportResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; namespace Bit.Api.SecretsManager.Models.Response; diff --git a/src/Api/SecretsManager/Models/Response/SMImportResponseModel.cs b/src/Api/SecretsManager/Models/Response/SMImportResponseModel.cs index 25d9956c43..46e7422c77 100644 --- a/src/Api/SecretsManager/Models/Response/SMImportResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SMImportResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Commands.Porting; namespace Bit.Api.SecretsManager.Models.Response; diff --git a/src/Api/SecretsManager/Models/Response/SecretWithProjectsListResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretWithProjectsListResponseModel.cs index 29dffa8e63..4f1e572a36 100644 --- a/src/Api/SecretsManager/Models/Response/SecretWithProjectsListResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SecretWithProjectsListResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountResponseModel.cs index 570c91fd08..17724d8fa0 100644 --- a/src/Api/SecretsManager/Models/Response/ServiceAccountResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Api/Utilities/ApiExplorerGroupConvention.cs b/src/Api/Utilities/ApiExplorerGroupConvention.cs index 42b1c8d6e7..e196b74617 100644 --- a/src/Api/Utilities/ApiExplorerGroupConvention.cs +++ b/src/Api/Utilities/ApiExplorerGroupConvention.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc.ApplicationModels; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace Bit.Api.Utilities; diff --git a/src/Api/Utilities/ApiHelpers.cs b/src/Api/Utilities/ApiHelpers.cs index 2c6dc8b73b..3c0701b1bd 100644 --- a/src/Api/Utilities/ApiHelpers.cs +++ b/src/Api/Utilities/ApiHelpers.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Messaging.EventGrid; using Azure.Messaging.EventGrid.SystemEvents; using Bit.Core.Exceptions; diff --git a/src/Api/Utilities/EnumMatchesAttribute.cs b/src/Api/Utilities/EnumMatchesAttribute.cs index a13b9d59d1..fb6a060170 100644 --- a/src/Api/Utilities/EnumMatchesAttribute.cs +++ b/src/Api/Utilities/EnumMatchesAttribute.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Utilities; diff --git a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs index 15e8bb2954..91079d5040 100644 --- a/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs +++ b/src/Api/Utilities/ExceptionHandlerFilterAttribute.cs @@ -1,4 +1,7 @@ -using System.Text; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text; using Bit.Api.Models.Public.Response; using Bit.Core.Billing; using Bit.Core.Exceptions; diff --git a/src/Api/Utilities/MultipartFormDataHelper.cs b/src/Api/Utilities/MultipartFormDataHelper.cs index a3eb64efb8..a2ead1368a 100644 --- a/src/Api/Utilities/MultipartFormDataHelper.cs +++ b/src/Api/Utilities/MultipartFormDataHelper.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Api.Tools.Models.Request; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.WebUtilities; diff --git a/src/Api/Utilities/PublicApiControllersModelConvention.cs b/src/Api/Utilities/PublicApiControllersModelConvention.cs index a7fabb0319..473485a67c 100644 --- a/src/Api/Utilities/PublicApiControllersModelConvention.cs +++ b/src/Api/Utilities/PublicApiControllersModelConvention.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc.ApplicationModels; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace Bit.Api.Utilities; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f2039cfbc9..7a3d462905 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; namespace Bit.Core; diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 68d4606907..85c8a81523 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.AdminConsole.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; diff --git a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs index 8be4a672db..c8d99c31f1 100644 --- a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs +++ b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; diff --git a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs index 283c07d199..f45c66ece8 100644 --- a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs +++ b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; namespace Bit.Core.Models.Api.Request.OrganizationSponsorships; diff --git a/src/Core/Models/Api/Request/PushDeviceRequestModel.cs b/src/Core/Models/Api/Request/PushDeviceRequestModel.cs index 8b97dcc360..c8ef83cadb 100644 --- a/src/Core/Models/Api/Request/PushDeviceRequestModel.cs +++ b/src/Core/Models/Api/Request/PushDeviceRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Models.Api; diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs index 0c87bf98d1..48afeacb21 100644 --- a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs +++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Enums; namespace Bit.Core.Models.Api; diff --git a/src/Core/Models/Api/Request/PushUpdateRequestModel.cs b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs index f8c2d296fd..e0a9696b38 100644 --- a/src/Core/Models/Api/Request/PushUpdateRequestModel.cs +++ b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Models.Api; diff --git a/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs b/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs index 573d77ab0c..bd59fa1921 100644 --- a/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs +++ b/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; namespace Bit.Core.Models.Api.Response.Duo; diff --git a/src/Core/Models/Api/Response/ErrorResponseModel.cs b/src/Core/Models/Api/Response/ErrorResponseModel.cs index 39d6adddb1..57c8259179 100644 --- a/src/Core/Models/Api/Response/ErrorResponseModel.cs +++ b/src/Core/Models/Api/Response/ErrorResponseModel.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Mvc.ModelBinding; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Core.Models.Api; diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs index e082d98de6..008637aa16 100644 --- a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; namespace Bit.Core.Models.Api.Response.OrganizationSponsorships; diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs index 5a6b635c5a..ac95d8095b 100644 --- a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; namespace Bit.Core.Models.Api.Response.OrganizationSponsorships; diff --git a/src/Core/Models/Business/CompleteSubscriptionUpdate.cs b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs index 1d983404af..7473738ffc 100644 --- a/src/Core/Models/Business/CompleteSubscriptionUpdate.cs +++ b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Exceptions; using Stripe; using Plan = Bit.Core.Models.StaticStore.Plan; diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index b8bd670d21..be79e71807 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; namespace Bit.Core.Models.Business; diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index 1dd2650799..89b9a5e6f2 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Enums; namespace Bit.Core.Models.Business; diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index 78a995fb94..a016ac54f3 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -1,4 +1,7 @@ -using Stripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Stripe; namespace Bit.Core.Models.Business; diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index f5afabfb9a..028fcad80b 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Enums; using Stripe; namespace Bit.Core.Models.Business; diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index 80a63473a7..4daa9a268a 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Business; public class TaxInfo { diff --git a/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs b/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs index 4bca8e1ca1..6c454154bb 100644 --- a/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs +++ b/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Tokens; diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 797aa6692a..da61369b24 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; diff --git a/src/Core/Models/Data/CollectionAccessDetails.cs b/src/Core/Models/Data/CollectionAccessDetails.cs index 447d55460c..5e294065e6 100644 --- a/src/Core/Models/Data/CollectionAccessDetails.cs +++ b/src/Core/Models/Data/CollectionAccessDetails.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data; public class CollectionAccessDetails { diff --git a/src/Core/Models/Data/InstallationDeviceEntity.cs b/src/Core/Models/Data/InstallationDeviceEntity.cs index a3d960b242..cafc1d1c03 100644 --- a/src/Core/Models/Data/InstallationDeviceEntity.cs +++ b/src/Core/Models/Data/InstallationDeviceEntity.cs @@ -1,4 +1,7 @@ -using Azure; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Azure; using Azure.Data.Tables; namespace Bit.Core.Models.Data; diff --git a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs index 7a9aa77110..dd7f04ac96 100644 --- a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; diff --git a/src/Core/Models/Data/Organizations/OrganizationDomainSsoDetailsData.cs b/src/Core/Models/Data/Organizations/OrganizationDomainSsoDetailsData.cs index b188f9403a..31f82e19a6 100644 --- a/src/Core/Models/Data/Organizations/OrganizationDomainSsoDetailsData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationDomainSsoDetailsData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data.Organizations; public class OrganizationDomainSsoDetailsData { diff --git a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs index 649459bc6b..62fbd90975 100644 --- a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships; diff --git a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs index 8c10187116..14562d54d9 100644 --- a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships; public class OrganizationSponsorshipSyncData { diff --git a/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs index 0a07af66b8..ec1986962a 100644 --- a/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs +++ b/src/Core/Models/Data/Organizations/VerifiedOrganizationDomainSsoDetail.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data.Organizations; public class VerifiedOrganizationDomainSsoDetail { diff --git a/src/Core/Models/Data/PageOptions.cs b/src/Core/Models/Data/PageOptions.cs index e9f12ece9a..16f049411a 100644 --- a/src/Core/Models/Data/PageOptions.cs +++ b/src/Core/Models/Data/PageOptions.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data; public class PageOptions { diff --git a/src/Core/Models/Data/PagedResult.cs b/src/Core/Models/Data/PagedResult.cs index b02044dd8c..dc272727a5 100644 --- a/src/Core/Models/Data/PagedResult.cs +++ b/src/Core/Models/Data/PagedResult.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Data; public class PagedResult { diff --git a/src/Core/Models/IExternal.cs b/src/Core/Models/IExternal.cs index e81de1d47b..4ea613c0b3 100644 --- a/src/Core/Models/IExternal.cs +++ b/src/Core/Models/IExternal.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models; public interface IExternal { diff --git a/src/Core/Models/Mail/AdminResetPasswordViewModel.cs b/src/Core/Models/Mail/AdminResetPasswordViewModel.cs index 18e257fea7..8ff58e54a2 100644 --- a/src/Core/Models/Mail/AdminResetPasswordViewModel.cs +++ b/src/Core/Models/Mail/AdminResetPasswordViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class AdminResetPasswordViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/BaseMailModel.cs b/src/Core/Models/Mail/BaseMailModel.cs index e3aa4d2c41..99873cf365 100644 --- a/src/Core/Models/Mail/BaseMailModel.cs +++ b/src/Core/Models/Mail/BaseMailModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class BaseMailModel { diff --git a/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs b/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs index a048312652..4fe42238e6 100644 --- a/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs +++ b/src/Core/Models/Mail/BaseTitleContactUsMailModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class BaseTitleContactUsMailModel : BaseMailModel { diff --git a/src/Core/Models/Mail/ChangeEmailExistsViewModel.cs b/src/Core/Models/Mail/ChangeEmailExistsViewModel.cs index 22367e8f27..c872ba0bbb 100644 --- a/src/Core/Models/Mail/ChangeEmailExistsViewModel.cs +++ b/src/Core/Models/Mail/ChangeEmailExistsViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class ChangeEmailExistsViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs index 97591b51bc..fa1ed5ab45 100644 --- a/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs +++ b/src/Core/Models/Mail/ClaimedDomainUserNotificationViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class ClaimedDomainUserNotificationViewModel : BaseTitleContactUsMailModel { diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs index 7e9d8ee193..adabbe0535 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.FamiliesForEnterprise; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.FamiliesForEnterprise; public class FamiliesForEnterpriseOfferViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRemoveOfferViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRemoveOfferViewModel.cs index 46cbb4d0a0..57c1a4bed5 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRemoveOfferViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRemoveOfferViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.FamiliesForEnterprise; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.FamiliesForEnterprise; public class FamiliesForEnterpriseRemoveOfferViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs index db62178a0a..50f8256b3d 100644 --- a/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs +++ b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class InvoiceUpcomingViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/LicenseExpiredViewModel.cs b/src/Core/Models/Mail/LicenseExpiredViewModel.cs index 922b35cfb1..e1d5578b80 100644 --- a/src/Core/Models/Mail/LicenseExpiredViewModel.cs +++ b/src/Core/Models/Mail/LicenseExpiredViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class LicenseExpiredViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/MailMessage.cs b/src/Core/Models/Mail/MailMessage.cs index df444c77f5..15e8e885cf 100644 --- a/src/Core/Models/Mail/MailMessage.cs +++ b/src/Core/Models/Mail/MailMessage.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class MailMessage { diff --git a/src/Core/Models/Mail/MailQueueMessage.cs b/src/Core/Models/Mail/MailQueueMessage.cs index d413c5f1a5..53f31becba 100644 --- a/src/Core/Models/Mail/MailQueueMessage.cs +++ b/src/Core/Models/Mail/MailQueueMessage.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Utilities; namespace Bit.Core.Models.Mail; diff --git a/src/Core/Models/Mail/NewDeviceLoggedInModel.cs b/src/Core/Models/Mail/NewDeviceLoggedInModel.cs index 6d55a19b64..0e4a49503a 100644 --- a/src/Core/Models/Mail/NewDeviceLoggedInModel.cs +++ b/src/Core/Models/Mail/NewDeviceLoggedInModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class NewDeviceLoggedInModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationDomainUnverifiedViewModel.cs b/src/Core/Models/Mail/OrganizationDomainUnverifiedViewModel.cs index a0547ed3a1..2d00c7056f 100644 --- a/src/Core/Models/Mail/OrganizationDomainUnverifiedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationDomainUnverifiedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationDomainUnverifiedViewModel { diff --git a/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs b/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs index 4e13abf656..4c4c265aba 100644 --- a/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs +++ b/src/Core/Models/Mail/OrganizationInitiateDeleteModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; diff --git a/src/Core/Models/Mail/OrganizationInvitesInfo.cs b/src/Core/Models/Mail/OrganizationInvitesInfo.cs index 267c386a66..d1c05605e5 100644 --- a/src/Core/Models/Mail/OrganizationInvitesInfo.cs +++ b/src/Core/Models/Mail/OrganizationInvitesInfo.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Models.Business; using Bit.Core.Billing.Enums; using Bit.Core.Entities; diff --git a/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs b/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs index 425b853d3e..1f393bf578 100644 --- a/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs +++ b/src/Core/Models/Mail/OrganizationSeatsAutoscaledViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationSeatsAutoscaledViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs b/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs index ad9c48ab31..24b65e807c 100644 --- a/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationSeatsMaxReachedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationSeatsMaxReachedViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs b/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs index c814a3e564..f60c5aeaaa 100644 --- a/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationServiceAccountsMaxReachedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationServiceAccountsMaxReachedViewModel { diff --git a/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs b/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs index 543df2fc65..80c22f287d 100644 --- a/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserAcceptedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserAcceptedViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs b/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs index 8254d3d841..a93d0bfdb4 100644 --- a/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserConfirmedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserConfirmedViewModel : BaseTitleContactUsMailModel { diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index f34f414ce8..82f05af9bd 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Core.Auth.Models.Business; using Bit.Core.Entities; using Bit.Core.Settings; diff --git a/src/Core/Models/Mail/OrganizationUserRemovedForPolicySingleOrgViewModel.cs b/src/Core/Models/Mail/OrganizationUserRemovedForPolicySingleOrgViewModel.cs index 46020ae46a..edebaab5b4 100644 --- a/src/Core/Models/Mail/OrganizationUserRemovedForPolicySingleOrgViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserRemovedForPolicySingleOrgViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserRemovedForPolicySingleOrgViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationUserRemovedForPolicyTwoStepViewModel.cs b/src/Core/Models/Mail/OrganizationUserRemovedForPolicyTwoStepViewModel.cs index cd4528ad50..6d87dd1b58 100644 --- a/src/Core/Models/Mail/OrganizationUserRemovedForPolicyTwoStepViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserRemovedForPolicyTwoStepViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserRemovedForPolicyTwoStepViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs b/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs index 27c784bd15..a278f6cc51 100644 --- a/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserRevokedForPolicySingleOrgViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserRevokedForPolicySingleOrgViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs b/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs index 9286ee74b3..d0eafbb2a9 100644 --- a/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserRevokedForPolicyTwoFactorViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class OrganizationUserRevokedForPolicyTwoFactorViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs b/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs index a5071527fe..851f76ac83 100644 --- a/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderInitiateDeleteModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs b/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs index f351a5fe1b..607f19c605 100644 --- a/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderSetupInviteViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderSetupInviteViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderUpdatePaymentMethodViewModel.cs b/src/Core/Models/Mail/Provider/ProviderUpdatePaymentMethodViewModel.cs index 114aaa7c95..ef21c0dcd5 100644 --- a/src/Core/Models/Mail/Provider/ProviderUpdatePaymentMethodViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderUpdatePaymentMethodViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderUpdatePaymentMethodViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs b/src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs index 30d24ad1e9..4cc7edfdbc 100644 --- a/src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderUserConfirmedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderUserConfirmedViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs b/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs index e418d30f21..0bce7c7005 100644 --- a/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderUserInvitedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderUserInvitedViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/Provider/ProviderUserRemovedViewModel.cs b/src/Core/Models/Mail/Provider/ProviderUserRemovedViewModel.cs index aef9d9c593..5753d0b317 100644 --- a/src/Core/Models/Mail/Provider/ProviderUserRemovedViewModel.cs +++ b/src/Core/Models/Mail/Provider/ProviderUserRemovedViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Provider; public class ProviderUserRemovedViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs index d41ca41146..e1dee2a89e 100644 --- a/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs +++ b/src/Core/Models/Mail/SecurityTaskNotificationViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class SecurityTaskNotificationViewModel : BaseMailModel { diff --git a/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs index 20c340acda..5265601984 100644 --- a/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs +++ b/src/Core/Models/Mail/TwoFactorEmailTokenViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; /// /// This view model is used to set-up email two factor authentication, to log in with email two factor authentication, diff --git a/src/Core/Models/Mail/UpdateTempPasswordViewModel.cs b/src/Core/Models/Mail/UpdateTempPasswordViewModel.cs index 6e45df5305..4c0c3519e0 100644 --- a/src/Core/Models/Mail/UpdateTempPasswordViewModel.cs +++ b/src/Core/Models/Mail/UpdateTempPasswordViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class UpdateTempPasswordViewModel { diff --git a/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs b/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs index b8850b5f00..43270efe38 100644 --- a/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs +++ b/src/Core/Models/Mail/UserVerificationEmailTokenViewModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail; public class UserVerificationEmailTokenViewModel : BaseMailModel { diff --git a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs index 07f07093d2..a72cebfbee 100644 --- a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs +++ b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.OrganizationConnectionConfigs; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.OrganizationConnectionConfigs; public class BillingSyncConfig : IConnectionConfig { diff --git a/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs b/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs index f32576c407..34662ecdbb 100644 --- a/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs +++ b/src/Core/Models/Stripe/StripeSubscriptionListOptions.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.BitStripe; +// 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. diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs index d83e30ad9c..e6f3489d2a 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/CreateCollectionCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs index b73afb4d1e..8a715c3052 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/ICreateCollectionCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Data; namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs index 94d4d1d1f8..14200ae7fc 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/Interfaces/IUpdateCollectionCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Data; namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; diff --git a/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs index 19ad47a0a5..0a03261330 100644 --- a/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationCollections/UpdateCollectionCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs index 111cec395c..713862154a 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs index e070b263a3..489b0c9021 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs index fc3f5b1321..b3675a1f0f 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Business.Tokenables; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs index 6b8d6d6771..dcda77acea 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Extensions; using Bit.Core.Entities; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs index 0d22b53bad..76e7b6bb2a 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.IdentityServer; using Bit.Core.Models.Api.Request.OrganizationSponsorships; diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index 91f6516501..88b995be64 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 761f59920c..284aaf7724 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs index 0d287a2229..6b76bc35f0 100644 --- a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs +++ b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs @@ -1,4 +1,7 @@ -using System.Text; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text; using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Bit.Core.Settings; diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs index 2685d36a7f..6b0027062c 100644 --- a/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.PhishingDomainFeatures.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.PhishingDomainFeatures.Interfaces; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; diff --git a/src/Core/SecretsManager/Commands/Porting/SMImport.cs b/src/Core/SecretsManager/Commands/Porting/SMImport.cs index 0e61b3acaf..80c6c65f6e 100644 --- a/src/Core/SecretsManager/Commands/Porting/SMImport.cs +++ b/src/Core/SecretsManager/Commands/Porting/SMImport.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.SecretsManager.Commands.Porting; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.SecretsManager.Commands.Porting; public class SMImport { diff --git a/src/Core/SecretsManager/Models/Data/ApiKeyClientSecretDetails.cs b/src/Core/SecretsManager/Models/Data/ApiKeyClientSecretDetails.cs index bcf84942b5..3cb2d22b37 100644 --- a/src/Core/SecretsManager/Models/Data/ApiKeyClientSecretDetails.cs +++ b/src/Core/SecretsManager/Models/Data/ApiKeyClientSecretDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs b/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs index 47fea5a52e..2210138f3b 100644 --- a/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs +++ b/src/Core/SecretsManager/Models/Data/ApiKeyDetails.cs @@ -1,4 +1,7 @@ -using System.Diagnostics.CodeAnalysis; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics.CodeAnalysis; using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs b/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs index db70312694..2678c10978 100644 --- a/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs +++ b/src/Core/SecretsManager/Models/Data/PeopleGrantees.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.SecretsManager.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.SecretsManager.Models.Data; public class PeopleGrantees { diff --git a/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs b/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs index ee3a4e6141..6ba1fbfd82 100644 --- a/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs +++ b/src/Core/SecretsManager/Models/Data/ProjectPeopleAccessPolicies.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/ProjectPermissionDetails.cs b/src/Core/SecretsManager/Models/Data/ProjectPermissionDetails.cs index 23c01a1fdf..7f847c816d 100644 --- a/src/Core/SecretsManager/Models/Data/ProjectPermissionDetails.cs +++ b/src/Core/SecretsManager/Models/Data/ProjectPermissionDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/SecretPermissionDetails.cs b/src/Core/SecretsManager/Models/Data/SecretPermissionDetails.cs index c5e15e25aa..232a96629a 100644 --- a/src/Core/SecretsManager/Models/Data/SecretPermissionDetails.cs +++ b/src/Core/SecretsManager/Models/Data/SecretPermissionDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs index b0cd37d5c0..e26de477ff 100644 --- a/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountPeopleAccessPolicies.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountSecretsDetails.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountSecretsDetails.cs index 67a369f02e..5fceac812d 100644 --- a/src/Core/SecretsManager/Models/Data/ServiceAccountSecretsDetails.cs +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountSecretsDetails.cs @@ -1,4 +1,7 @@ -using Bit.Core.SecretsManager.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs b/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs index 1e35f97d1d..59bad9595c 100644 --- a/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs +++ b/src/Core/SecretsManager/Models/Mail/RequestSecretsManagerAccessViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Mail; namespace Bit.Core.SecretsManager.Models.Mail; diff --git a/src/Core/SecretsManager/Repositories/IProjectRepository.cs b/src/Core/SecretsManager/Repositories/IProjectRepository.cs index 7a084b42cc..93dabacb49 100644 --- a/src/Core/SecretsManager/Repositories/IProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/IProjectRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Repositories/ISecretRepository.cs b/src/Core/SecretsManager/Repositories/ISecretRepository.cs index 20ebb61e9a..0456e41ed5 100644 --- a/src/Core/SecretsManager/Repositories/ISecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/ISecretRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates; diff --git a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs index a2d12578d5..26f01a4737 100644 --- a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs index 439b32197a..043230a009 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs index 2d434df597..39f5e3d19e 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates; diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs index 7155608bcf..335ec4b2e3 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; diff --git a/src/Core/Services/IFeatureService.cs b/src/Core/Services/IFeatureService.cs index 0ac168a0cd..d1a7344ddb 100644 --- a/src/Core/Services/IFeatureService.cs +++ b/src/Core/Services/IFeatureService.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Services; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Services; public interface IFeatureService { diff --git a/src/Core/Services/II18nService.cs b/src/Core/Services/II18nService.cs index ee92664d88..9c20fc3d95 100644 --- a/src/Core/Services/II18nService.cs +++ b/src/Core/Services/II18nService.cs @@ -1,11 +1,13 @@ -using Microsoft.Extensions.Localization; +#nullable enable + +using Microsoft.Extensions.Localization; namespace Bit.Core.Services; public interface II18nService { LocalizedString GetLocalizedHtmlString(string key); - LocalizedString GetLocalizedHtmlString(string key, params object[] args); - string Translate(string key, params object[] args); - string T(string key, params object[] args); + LocalizedString GetLocalizedHtmlString(string key, params object?[] args); + string Translate(string key, params object?[] args); + string T(string key, params object?[] args); } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index af96b88ee6..9b56399add 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Models.Business; using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Requests; diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index 1ba93da4fa..2b2bf8d825 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.BitStripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.BitStripe; using Stripe; namespace Bit.Core.Services; diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 2ac9345ebf..43c204e513 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; diff --git a/src/Core/Services/Implementations/BaseIdentityClientService.cs b/src/Core/Services/Implementations/BaseIdentityClientService.cs index f6d623692d..7281799d2f 100644 --- a/src/Core/Services/Implementations/BaseIdentityClientService.cs +++ b/src/Core/Services/Implementations/BaseIdentityClientService.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; diff --git a/src/Core/Services/Implementations/I18nService.cs b/src/Core/Services/Implementations/I18nService.cs index 25e2f8e5dc..3d8737dbaa 100644 --- a/src/Core/Services/Implementations/I18nService.cs +++ b/src/Core/Services/Implementations/I18nService.cs @@ -20,17 +20,19 @@ public class I18nService : II18nService return _localizer[key]; } - public LocalizedString GetLocalizedHtmlString(string key, params object[] args) + public LocalizedString GetLocalizedHtmlString(string key, params object?[] args) { +#nullable disable // IStringLocalizer does actually support null args, it is annotated incorrectly: https://github.com/dotnet/aspnetcore/issues/44251 return _localizer[key, args]; +#nullable enable } - public string Translate(string key, params object[] args) + public string Translate(string key, params object?[] args) { return string.Format(GetLocalizedHtmlString(key).ToString(), args); } - public string T(string key, params object[] args) + public string T(string key, params object?[] args) { return Translate(key, args); } diff --git a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs index 0fde6d8906..d1bece56c1 100644 --- a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs index 69b8a94e5a..1fb2348c5a 100644 --- a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs +++ b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Context; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Context; using Bit.Core.Identity; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 2d91017ce2..ca607bb5b4 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -1,4 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Text; diff --git a/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs b/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs index 2ebc7492f7..f12714e462 100644 --- a/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs +++ b/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs @@ -1,4 +1,7 @@ -using System.Security.Cryptography.X509Certificates; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Cryptography.X509Certificates; using Bit.Core.Platform.X509ChainCustomization; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index fd9f212ee7..9315d92ebe 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.BitStripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.BitStripe; using Stripe; namespace Bit.Core.Services; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index bdd558df52..846b9b94c8 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Models.Business; using Bit.Core.Billing.Constants; diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 1e8acf0a15..9ae10e333f 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index ba6d4e692e..e4f308c358 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Settings; using Bit.Core.Settings.LoggingSettings; namespace Bit.Core.Settings; diff --git a/src/Core/Tokens/DataProtectorTokenFactory.cs b/src/Core/Tokens/DataProtectorTokenFactory.cs index 1f8c2254f3..26cf517dfc 100644 --- a/src/Core/Tokens/DataProtectorTokenFactory.cs +++ b/src/Core/Tokens/DataProtectorTokenFactory.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.DataProtection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; namespace Bit.Core.Tokens; diff --git a/src/Core/Tokens/Tokenable.cs b/src/Core/Tokens/Tokenable.cs index a145e64bb5..860982228f 100644 --- a/src/Core/Tokens/Tokenable.cs +++ b/src/Core/Tokens/Tokenable.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; namespace Bit.Core.Tokens; diff --git a/src/Core/Utilities/AssemblyHelpers.cs b/src/Core/Utilities/AssemblyHelpers.cs index a00e108515..0cc01efdf3 100644 --- a/src/Core/Utilities/AssemblyHelpers.cs +++ b/src/Core/Utilities/AssemblyHelpers.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/BitPayClient.cs b/src/Core/Utilities/BitPayClient.cs index 35a078998d..cf241d5723 100644 --- a/src/Core/Utilities/BitPayClient.cs +++ b/src/Core/Utilities/BitPayClient.cs @@ -1,4 +1,7 @@ -using Bit.Core.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Settings; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/BulkAuthorizationHandler.cs b/src/Core/Utilities/BulkAuthorizationHandler.cs index c427a426e0..bb5764c53c 100644 --- a/src/Core/Utilities/BulkAuthorizationHandler.cs +++ b/src/Core/Utilities/BulkAuthorizationHandler.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Authorization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Authorization; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/CustomRedisProcessingStrategy.cs b/src/Core/Utilities/CustomRedisProcessingStrategy.cs index 12a48e400f..b7125bfc79 100644 --- a/src/Core/Utilities/CustomRedisProcessingStrategy.cs +++ b/src/Core/Utilities/CustomRedisProcessingStrategy.cs @@ -1,4 +1,7 @@ -using AspNetCoreRateLimit; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AspNetCoreRateLimit; using AspNetCoreRateLimit.Redis; using Bit.Core.Settings; using Microsoft.Extensions.Caching.Memory; diff --git a/src/Core/Utilities/DistributedCacheExtensions.cs b/src/Core/Utilities/DistributedCacheExtensions.cs index 28282b6a47..2459faeb56 100644 --- a/src/Core/Utilities/DistributedCacheExtensions.cs +++ b/src/Core/Utilities/DistributedCacheExtensions.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Microsoft.Extensions.Caching.Distributed; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/HandlebarsObjectJsonConverter.cs b/src/Core/Utilities/HandlebarsObjectJsonConverter.cs index 5651da4dc9..895c0ba263 100644 --- a/src/Core/Utilities/HandlebarsObjectJsonConverter.cs +++ b/src/Core/Utilities/HandlebarsObjectJsonConverter.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using System.Text.Json.Serialization; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/JsonHelpers.cs b/src/Core/Utilities/JsonHelpers.cs index 3f06794b7c..af3964defd 100644 --- a/src/Core/Utilities/JsonHelpers.cs +++ b/src/Core/Utilities/JsonHelpers.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Net; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index b2388bc499..5809da9c7a 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -1,4 +1,7 @@ -using System.Security.Cryptography.X509Certificates; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Cryptography.X509Certificates; using Bit.Core.Settings; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 1cae361e29..1ddd926569 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,4 +1,7 @@ -using System.Collections.Immutable; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Collections.Immutable; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models.StaticStore.Plans; diff --git a/src/Core/Utilities/StrictEmailAddressAttribute.cs b/src/Core/Utilities/StrictEmailAddressAttribute.cs index fce732ec9e..64c95f8796 100644 --- a/src/Core/Utilities/StrictEmailAddressAttribute.cs +++ b/src/Core/Utilities/StrictEmailAddressAttribute.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/StrictEmailAddressListAttribute.cs b/src/Core/Utilities/StrictEmailAddressListAttribute.cs index 456980397a..ab13f9a819 100644 --- a/src/Core/Utilities/StrictEmailAddressListAttribute.cs +++ b/src/Core/Utilities/StrictEmailAddressListAttribute.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Utilities; diff --git a/src/Core/Utilities/SystemTextJsonCosmosSerializer.cs b/src/Core/Utilities/SystemTextJsonCosmosSerializer.cs index 8b2b8684e5..009dcc11e7 100644 --- a/src/Core/Utilities/SystemTextJsonCosmosSerializer.cs +++ b/src/Core/Utilities/SystemTextJsonCosmosSerializer.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Core.Serialization; using Microsoft.Azure.Cosmos; diff --git a/src/Icons/Controllers/IconsController.cs b/src/Icons/Controllers/IconsController.cs index 871219b366..0d32a8254b 100644 --- a/src/Icons/Controllers/IconsController.cs +++ b/src/Icons/Controllers/IconsController.cs @@ -1,4 +1,7 @@ -using Bit.Icons.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Icons.Models; using Bit.Icons.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; diff --git a/src/Icons/Models/DomainName.cs b/src/Icons/Models/DomainName.cs index b040110504..8b90dff42d 100644 --- a/src/Icons/Models/DomainName.cs +++ b/src/Icons/Models/DomainName.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using System.Reflection; using System.Text.RegularExpressions; diff --git a/src/Icons/Models/Icon.cs b/src/Icons/Models/Icon.cs index 8bd23541fa..396a105716 100644 --- a/src/Icons/Models/Icon.cs +++ b/src/Icons/Models/Icon.cs @@ -1,4 +1,7 @@ -namespace Bit.Icons.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Icons.Models; public class Icon { diff --git a/src/Infrastructure.Dapper/SecretsManager/Repositories/ApiKeyRepository.cs b/src/Infrastructure.Dapper/SecretsManager/Repositories/ApiKeyRepository.cs index c362ae2369..52309344f7 100644 --- a/src/Infrastructure.Dapper/SecretsManager/Repositories/ApiKeyRepository.cs +++ b/src/Infrastructure.Dapper/SecretsManager/Repositories/ApiKeyRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; diff --git a/src/Infrastructure.EntityFramework/Converters/DataProtectionConverter.cs b/src/Infrastructure.EntityFramework/Converters/DataProtectionConverter.cs index ee5c23fa71..a3c55fd536 100644 --- a/src/Infrastructure.EntityFramework/Converters/DataProtectionConverter.cs +++ b/src/Infrastructure.EntityFramework/Converters/DataProtectionConverter.cs @@ -1,4 +1,7 @@ -using Bit.Core; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs index 9aaec0af2c..743345f7fd 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationApplication.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Dirt.Models; diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs index a7d08e142f..0b58d433ff 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Models/OrganizationReport.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Dirt.Models; diff --git a/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs b/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs index bc471f0844..e8fc818b28 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Models/PasswordHealthReportApplication.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Dirt.Models; diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs index 416fd91933..c8e5432e03 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; diff --git a/src/Infrastructure.EntityFramework/Models/Collection.cs b/src/Infrastructure.EntityFramework/Models/Collection.cs index 8418c33703..2057b43ac1 100644 --- a/src/Infrastructure.EntityFramework/Models/Collection.cs +++ b/src/Infrastructure.EntityFramework/Models/Collection.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/CollectionCipher.cs b/src/Infrastructure.EntityFramework/Models/CollectionCipher.cs index 4058ddc030..f302685d68 100644 --- a/src/Infrastructure.EntityFramework/Models/CollectionCipher.cs +++ b/src/Infrastructure.EntityFramework/Models/CollectionCipher.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Vault.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/CollectionGroup.cs b/src/Infrastructure.EntityFramework/Models/CollectionGroup.cs index 623a5d8084..f86d89fa33 100644 --- a/src/Infrastructure.EntityFramework/Models/CollectionGroup.cs +++ b/src/Infrastructure.EntityFramework/Models/CollectionGroup.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/CollectionUser.cs b/src/Infrastructure.EntityFramework/Models/CollectionUser.cs index 308673492b..7779d13912 100644 --- a/src/Infrastructure.EntityFramework/Models/CollectionUser.cs +++ b/src/Infrastructure.EntityFramework/Models/CollectionUser.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/Device.cs b/src/Infrastructure.EntityFramework/Models/Device.cs index 1eace238d5..06054293c8 100644 --- a/src/Infrastructure.EntityFramework/Models/Device.cs +++ b/src/Infrastructure.EntityFramework/Models/Device.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/Group.cs b/src/Infrastructure.EntityFramework/Models/Group.cs index 7a537cfcf4..77f59615dd 100644 --- a/src/Infrastructure.EntityFramework/Models/Group.cs +++ b/src/Infrastructure.EntityFramework/Models/Group.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/GroupUser.cs b/src/Infrastructure.EntityFramework/Models/GroupUser.cs index 4499b20f8a..57b0610708 100644 --- a/src/Infrastructure.EntityFramework/Models/GroupUser.cs +++ b/src/Infrastructure.EntityFramework/Models/GroupUser.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs b/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs index e13bde5fb3..280bf3c7ed 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs b/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs index 5635bbba7e..0acf783ecb 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationDomain.cs b/src/Infrastructure.EntityFramework/Models/OrganizationDomain.cs index 0d9ccefca0..0963d2c119 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationDomain.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationDomain.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs b/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs index 4780346a1f..85504286f6 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationUser.cs b/src/Infrastructure.EntityFramework/Models/OrganizationUser.cs index 805dec49f7..79bb01fc50 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationUser.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationUser.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Models/Transaction.cs b/src/Infrastructure.EntityFramework/Models/Transaction.cs index c12654343e..2733609fb7 100644 --- a/src/Infrastructure.EntityFramework/Models/Transaction.cs +++ b/src/Infrastructure.EntityFramework/Models/Transaction.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; diff --git a/src/Infrastructure.EntityFramework/Models/User.cs b/src/Infrastructure.EntityFramework/Models/User.cs index 9e33d9edf6..89e6f35739 100644 --- a/src/Infrastructure.EntityFramework/Models/User.cs +++ b/src/Infrastructure.EntityFramework/Models/User.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Models/Notification.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Models/Notification.cs index ec8db45c5a..af8f7ab295 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Models/Notification.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Models/Notification.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Models/NotificationStatus.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Models/NotificationStatus.cs index 2a2f8c0ef6..d298708311 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Models/NotificationStatus.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Models/NotificationStatus.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.NotificationCenter.Models; diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs index 507849f51b..98d555ff19 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/AccessPolicy.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/AccessPolicy.cs index 9eca8e5729..769746c27f 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/AccessPolicy.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/AccessPolicy.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/ApiKey.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/ApiKey.cs index 7d2c05f147..9ec45486a0 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/ApiKey.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/ApiKey.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/Project.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/Project.cs index 77ca602841..05035b2f37 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/Project.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/Project.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs index 58dcfce41f..5992f32135 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/Secret.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs index 812740e7ae..fa42c16bf3 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.SecretsManager.Models; diff --git a/src/Notifications/AnonymousNotificationsHub.cs b/src/Notifications/AnonymousNotificationsHub.cs index e3e7d478c8..ae17de1af3 100644 --- a/src/Notifications/AnonymousNotificationsHub.cs +++ b/src/Notifications/AnonymousNotificationsHub.cs @@ -1,4 +1,7 @@ -using Microsoft.AspNetCore.Authorization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; diff --git a/src/Notifications/AzureQueueHostedService.cs b/src/Notifications/AzureQueueHostedService.cs index 977d9a9d1d..c67e6b6986 100644 --- a/src/Notifications/AzureQueueHostedService.cs +++ b/src/Notifications/AzureQueueHostedService.cs @@ -1,4 +1,7 @@ -using Azure.Storage.Queues; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Azure.Storage.Queues; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.SignalR; diff --git a/src/Notifications/HeartbeatHostedService.cs b/src/Notifications/HeartbeatHostedService.cs index 6dcfe7189f..e69cab3e78 100644 --- a/src/Notifications/HeartbeatHostedService.cs +++ b/src/Notifications/HeartbeatHostedService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Settings; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index c8ce3ecfe5..f49ca96ea4 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models; using Microsoft.AspNetCore.SignalR; diff --git a/src/Notifications/NotificationsHub.cs b/src/Notifications/NotificationsHub.cs index ed62dbbd66..bc123fcf84 100644 --- a/src/Notifications/NotificationsHub.cs +++ b/src/Notifications/NotificationsHub.cs @@ -1,4 +1,7 @@ -using Bit.Core.Context; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Notifications/SubjectUserIdProvider.cs b/src/Notifications/SubjectUserIdProvider.cs index 261394d06c..b0873eb2ec 100644 --- a/src/Notifications/SubjectUserIdProvider.cs +++ b/src/Notifications/SubjectUserIdProvider.cs @@ -1,4 +1,7 @@ -using IdentityModel; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using IdentityModel; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; diff --git a/src/SharedWeb/Health/HealthCheckServiceExtensions.cs b/src/SharedWeb/Health/HealthCheckServiceExtensions.cs index 9be369c676..4fa8d71ca0 100644 --- a/src/SharedWeb/Health/HealthCheckServiceExtensions.cs +++ b/src/SharedWeb/Health/HealthCheckServiceExtensions.cs @@ -1,4 +1,7 @@ -using System.Text; +// 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.Core.Settings; using Microsoft.AspNetCore.Http; diff --git a/src/SharedWeb/Utilities/DisplayAttributeHelpers.cs b/src/SharedWeb/Utilities/DisplayAttributeHelpers.cs index 4a48a2d164..33cd5cb91e 100644 --- a/src/SharedWeb/Utilities/DisplayAttributeHelpers.cs +++ b/src/SharedWeb/Utilities/DisplayAttributeHelpers.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; namespace Bit.SharedWeb.Utilities; diff --git a/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs b/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs index f43544bca4..332aa6838c 100644 --- a/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs +++ b/src/SharedWeb/Utilities/ExceptionHandlerFilterAttribute.cs @@ -1,4 +1,7 @@ -using Bit.Core.Exceptions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Exceptions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 1ff7943378..0bf09706a9 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using System.Reflection; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; diff --git a/test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs index 7e6f81c30a..41ac06c03d 100644 --- a/test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; using AutoFixture; using Bit.Test.Common.Helpers; using Xunit; diff --git a/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs index 1b6adb262f..40a8da48c5 100644 --- a/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs @@ -1,4 +1,7 @@ -using AutoFixture; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoFixture; using AutoFixture.Xunit2; namespace Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Common/AutoFixture/Attributes/EnvironmentDataAttribute.cs b/test/Common/AutoFixture/Attributes/EnvironmentDataAttribute.cs index acdf737be8..d3a8368545 100644 --- a/test/Common/AutoFixture/Attributes/EnvironmentDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/EnvironmentDataAttribute.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; using Xunit.Sdk; namespace Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs index 41b1dc63b4..fd30abefbc 100644 --- a/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs +++ b/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs @@ -1,4 +1,7 @@ -using AutoFixture; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoFixture; using Bit.Test.Common.AutoFixture.JsonDocumentFixtures; namespace Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs b/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs index 039475fadc..2d16ac4500 100644 --- a/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs +++ b/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs @@ -1,4 +1,7 @@ -using AutoFixture; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoFixture; using AutoFixture.Kernel; namespace Bit.Test.Common.AutoFixture; diff --git a/test/Common/AutoFixture/JsonDocumentFixtures.cs b/test/Common/AutoFixture/JsonDocumentFixtures.cs index df27aa8ce7..79f4ab8d9c 100644 --- a/test/Common/AutoFixture/JsonDocumentFixtures.cs +++ b/test/Common/AutoFixture/JsonDocumentFixtures.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using AutoFixture; using AutoFixture.Kernel; diff --git a/test/Common/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs index bdab622754..e1b37a9827 100644 --- a/test/Common/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; using AutoFixture; using AutoFixture.Kernel; diff --git a/test/Common/AutoFixture/SutProviderCustomization.cs b/test/Common/AutoFixture/SutProviderCustomization.cs index 5cbff6a718..af5e2f73df 100644 --- a/test/Common/AutoFixture/SutProviderCustomization.cs +++ b/test/Common/AutoFixture/SutProviderCustomization.cs @@ -1,4 +1,7 @@ -using AutoFixture; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoFixture; using AutoFixture.Kernel; namespace Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Common/AutoFixture/SutProviderExtensions.cs b/test/Common/AutoFixture/SutProviderExtensions.cs index bdc8604166..1a7aaf62e6 100644 --- a/test/Common/AutoFixture/SutProviderExtensions.cs +++ b/test/Common/AutoFixture/SutProviderExtensions.cs @@ -1,4 +1,7 @@ -using AutoFixture; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoFixture; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Time.Testing; diff --git a/test/Common/Fakes/FakeDataProtectorTokenFactory.cs b/test/Common/Fakes/FakeDataProtectorTokenFactory.cs index fe3af320d6..0fb8717abf 100644 --- a/test/Common/Fakes/FakeDataProtectorTokenFactory.cs +++ b/test/Common/Fakes/FakeDataProtectorTokenFactory.cs @@ -1,4 +1,7 @@ -using Bit.Core.Tokens; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Tokens; namespace Bit.Test.Common.Fakes; diff --git a/test/Common/Helpers/AssertHelper.cs b/test/Common/Helpers/AssertHelper.cs index 0b6752a036..5e9c3a5aba 100644 --- a/test/Common/Helpers/AssertHelper.cs +++ b/test/Common/Helpers/AssertHelper.cs @@ -1,4 +1,7 @@ -using System.Collections; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Collections; using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; diff --git a/test/Common/MockedHttpClient/HttpResponseBuilder.cs b/test/Common/MockedHttpClient/HttpResponseBuilder.cs index 067defb6d2..c90d6eb46b 100644 --- a/test/Common/MockedHttpClient/HttpResponseBuilder.cs +++ b/test/Common/MockedHttpClient/HttpResponseBuilder.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; namespace Bit.Test.Common.MockedHttpClient; diff --git a/test/Common/MockedHttpClient/MockedHttpResponse.cs b/test/Common/MockedHttpClient/MockedHttpResponse.cs index 499807c615..23076e67c4 100644 --- a/test/Common/MockedHttpClient/MockedHttpResponse.cs +++ b/test/Common/MockedHttpClient/MockedHttpResponse.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using System.Net.Http.Headers; using System.Text; diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index eced27f937..97a836cf44 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -1,4 +1,7 @@ -using System.Collections.Concurrent; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Collections.Concurrent; using System.Net.Http.Json; using System.Text.Json; using Bit.Core.Auth.Models.Api.Request.Accounts; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs index 128b38ff9a..3f5bf49dd9 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Primitives; diff --git a/test/IntegrationTestCommon/FakeRemoteIpAddressMiddleware.cs b/test/IntegrationTestCommon/FakeRemoteIpAddressMiddleware.cs index 69696f67c2..213345ef79 100644 --- a/test/IntegrationTestCommon/FakeRemoteIpAddressMiddleware.cs +++ b/test/IntegrationTestCommon/FakeRemoteIpAddressMiddleware.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.IntegrationTestCommon.Factories; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; diff --git a/test/IntegrationTestCommon/SqlServerTestDatabase.cs b/test/IntegrationTestCommon/SqlServerTestDatabase.cs index 93ffa90d3d..30eb1f5a5a 100644 --- a/test/IntegrationTestCommon/SqlServerTestDatabase.cs +++ b/test/IntegrationTestCommon/SqlServerTestDatabase.cs @@ -1,4 +1,7 @@ -using Bit.Core.Settings; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Settings; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Migrator; using Microsoft.Data.SqlClient; diff --git a/util/Migrator/DbMigrator.cs b/util/Migrator/DbMigrator.cs index b9584326e9..e5e7a569b2 100644 --- a/util/Migrator/DbMigrator.cs +++ b/util/Migrator/DbMigrator.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using System.Reflection; using System.Text; using Bit.Core; diff --git a/util/Server/Program.cs b/util/Server/Program.cs index 767b965149..a2d7e5f687 100644 --- a/util/Server/Program.cs +++ b/util/Server/Program.cs @@ -1,4 +1,7 @@ -namespace Bit.Server; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Server; public class Program { diff --git a/util/Setup/AppIdBuilder.cs b/util/Setup/AppIdBuilder.cs index 6e984aa904..59f513b434 100644 --- a/util/Setup/AppIdBuilder.cs +++ b/util/Setup/AppIdBuilder.cs @@ -1,4 +1,7 @@ -namespace Bit.Setup; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Setup; public class AppIdBuilder { diff --git a/util/Setup/Configuration.cs b/util/Setup/Configuration.cs index 3372652d03..00a1ef1eca 100644 --- a/util/Setup/Configuration.cs +++ b/util/Setup/Configuration.cs @@ -1,4 +1,7 @@ -using System.ComponentModel; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel; using YamlDotNet.Serialization; namespace Bit.Setup; diff --git a/util/Setup/Context.cs b/util/Setup/Context.cs index 58a5ced0ac..a5222a4289 100644 --- a/util/Setup/Context.cs +++ b/util/Setup/Context.cs @@ -1,4 +1,7 @@ -using Bit.Setup.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Setup.Enums; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/util/Setup/EnvironmentFileBuilder.cs b/util/Setup/EnvironmentFileBuilder.cs index 9c6471cb30..e3166cd57b 100644 --- a/util/Setup/EnvironmentFileBuilder.cs +++ b/util/Setup/EnvironmentFileBuilder.cs @@ -1,4 +1,7 @@ -using Microsoft.Data.SqlClient; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.Data.SqlClient; namespace Bit.Setup; diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs index d5c0417de6..07a8e0b1ef 100644 --- a/util/Setup/Helpers.cs +++ b/util/Setup/Helpers.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs index 1315ffaba7..ecaa4280b3 100644 --- a/util/Setup/NginxConfigBuilder.cs +++ b/util/Setup/NginxConfigBuilder.cs @@ -1,4 +1,7 @@ -namespace Bit.Setup; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Setup; public class NginxConfigBuilder { diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index 921c32f5e6..416f449de7 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Net.Http.Json; using Bit.Migrator; using Bit.Setup.Enums; diff --git a/util/Setup/YamlComments.cs b/util/Setup/YamlComments.cs index 5bdb6fddf9..cd50495349 100644 --- a/util/Setup/YamlComments.cs +++ b/util/Setup/YamlComments.cs @@ -1,4 +1,7 @@ -using System.ComponentModel; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; From 50461518e7be230a50319193b74244380e2b3575 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:46:13 -0400 Subject: [PATCH 029/326] Add `#nullable disable` to vault code (#6053) --- src/Api/Vault/Controllers/CiphersController.cs | 5 ++++- src/Api/Vault/Controllers/FoldersController.cs | 5 ++++- src/Api/Vault/Controllers/SecurityTaskController.cs | 5 ++++- src/Api/Vault/Controllers/SyncController.cs | 5 ++++- src/Api/Vault/Models/CipherAttachmentModel.cs | 5 ++++- src/Api/Vault/Models/CipherCardModel.cs | 5 ++++- src/Api/Vault/Models/CipherFido2CredentialModel.cs | 5 ++++- src/Api/Vault/Models/CipherFieldModel.cs | 5 ++++- src/Api/Vault/Models/CipherIdentityModel.cs | 5 ++++- src/Api/Vault/Models/CipherLoginModel.cs | 5 ++++- src/Api/Vault/Models/CipherPasswordHistoryModel.cs | 5 ++++- src/Api/Vault/Models/CipherSSHKeyModel.cs | 5 ++++- src/Api/Vault/Models/Request/AttachmentRequestModel.cs | 5 ++++- .../Models/Request/BulkCreateSecurityTasksRequestModel.cs | 5 ++++- .../Request/CipherBulkUpdateCollectionsRequestModel.cs | 5 ++++- src/Api/Vault/Models/Request/CipherPartialRequestModel.cs | 5 ++++- src/Api/Vault/Models/Request/CipherRequestModel.cs | 5 ++++- src/Api/Vault/Models/Request/FolderRequestModel.cs | 5 ++++- src/Api/Vault/Models/Response/AttachmentResponseModel.cs | 5 ++++- .../Models/Response/AttachmentUploadDataResponseModel.cs | 5 ++++- .../Vault/Models/Response/CipherPermissionsResponseModel.cs | 5 ++++- src/Api/Vault/Models/Response/CipherResponseModel.cs | 5 ++++- src/Api/Vault/Models/Response/FolderResponseModel.cs | 5 ++++- src/Api/Vault/Models/Response/SyncResponseModel.cs | 5 ++++- .../Vault/Commands/CreateManyTaskNotificationsCommand.cs | 5 ++++- src/Core/Vault/Entities/Cipher.cs | 5 ++++- src/Core/Vault/Models/Data/AttachmentResponseData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherAttachment.cs | 5 ++++- src/Core/Vault/Models/Data/CipherCardData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherFieldData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherIdentityData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherLoginData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs | 5 ++++- src/Core/Vault/Models/Data/CipherSSHKeyData.cs | 5 ++++- src/Core/Vault/Models/Data/UserCipherForTask.cs | 5 ++++- src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs | 5 ++++- src/Core/Vault/Models/Data/UserSecurityTasksCount.cs | 5 ++++- src/Core/Vault/Services/ICipherService.cs | 5 ++++- .../Implementations/AzureAttachmentStorageService.cs | 5 ++++- src/Core/Vault/Services/Implementations/CipherService.cs | 5 ++++- .../NoopImplementations/NoopAttachmentStorageService.cs | 5 ++++- src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs | 5 ++++- src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs | 5 ++++- .../Vault/Repositories/CipherRepository.cs | 5 ++++- .../Vault/Repositories/FolderRepository.cs | 5 ++++- src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs | 5 ++++- src/Infrastructure.EntityFramework/Vault/Models/Folder.cs | 5 ++++- .../Vault/Models/SecurityTask.cs | 5 ++++- .../Vault/Repositories/CipherRepository.cs | 5 ++++- .../Vault/Repositories/FolderRepository.cs | 5 ++++- .../Queries/UserSecurityTasksByCipherIdsQuery.cs | 5 ++++- 53 files changed, 212 insertions(+), 53 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 5991d0babb..e9a3fac08f 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Messaging.EventGrid; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Models.Response; diff --git a/src/Api/Vault/Controllers/FoldersController.cs b/src/Api/Vault/Controllers/FoldersController.cs index da9e6760c6..9da9e6a184 100644 --- a/src/Api/Vault/Controllers/FoldersController.cs +++ b/src/Api/Vault/Controllers/FoldersController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core.Exceptions; diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index d94c9a9a92..7f61271ab2 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core.Services; diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 568c05d651..54f1b9e70b 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Vault.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; diff --git a/src/Api/Vault/Models/CipherAttachmentModel.cs b/src/Api/Vault/Models/CipherAttachmentModel.cs index 1eadfc8ef5..381f66d37d 100644 --- a/src/Api/Vault/Models/CipherAttachmentModel.cs +++ b/src/Api/Vault/Models/CipherAttachmentModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; namespace Bit.Api.Vault.Models; diff --git a/src/Api/Vault/Models/CipherCardModel.cs b/src/Api/Vault/Models/CipherCardModel.cs index 5389de321e..e89dd51330 100644 --- a/src/Api/Vault/Models/CipherCardModel.cs +++ b/src/Api/Vault/Models/CipherCardModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherFido2CredentialModel.cs b/src/Api/Vault/Models/CipherFido2CredentialModel.cs index 09d66a22e5..0133173171 100644 --- a/src/Api/Vault/Models/CipherFido2CredentialModel.cs +++ b/src/Api/Vault/Models/CipherFido2CredentialModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherFieldModel.cs b/src/Api/Vault/Models/CipherFieldModel.cs index d51a766f7a..93abf9f647 100644 --- a/src/Api/Vault/Models/CipherFieldModel.cs +++ b/src/Api/Vault/Models/CipherFieldModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Utilities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherIdentityModel.cs b/src/Api/Vault/Models/CipherIdentityModel.cs index ea32bab93d..6f70a3cc49 100644 --- a/src/Api/Vault/Models/CipherIdentityModel.cs +++ b/src/Api/Vault/Models/CipherIdentityModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherLoginModel.cs b/src/Api/Vault/Models/CipherLoginModel.cs index 9580ebfed4..fc0aad14f8 100644 --- a/src/Api/Vault/Models/CipherLoginModel.cs +++ b/src/Api/Vault/Models/CipherLoginModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherPasswordHistoryModel.cs b/src/Api/Vault/Models/CipherPasswordHistoryModel.cs index 6c70acb049..f9e9eff186 100644 --- a/src/Api/Vault/Models/CipherPasswordHistoryModel.cs +++ b/src/Api/Vault/Models/CipherPasswordHistoryModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/CipherSSHKeyModel.cs b/src/Api/Vault/Models/CipherSSHKeyModel.cs index 47853aa36e..850ffb656c 100644 --- a/src/Api/Vault/Models/CipherSSHKeyModel.cs +++ b/src/Api/Vault/Models/CipherSSHKeyModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; namespace Bit.Api.Vault.Models; diff --git a/src/Api/Vault/Models/Request/AttachmentRequestModel.cs b/src/Api/Vault/Models/Request/AttachmentRequestModel.cs index e66cd56f29..96c66c6044 100644 --- a/src/Api/Vault/Models/Request/AttachmentRequestModel.cs +++ b/src/Api/Vault/Models/Request/AttachmentRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.Vault.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.Vault.Models.Request; public class AttachmentRequestModel { diff --git a/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs index 6c8c7e03b3..d269840298 100644 --- a/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs +++ b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Vault.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Vault.Models.Api; namespace Bit.Api.Vault.Models.Request; diff --git a/src/Api/Vault/Models/Request/CipherBulkUpdateCollectionsRequestModel.cs b/src/Api/Vault/Models/Request/CipherBulkUpdateCollectionsRequestModel.cs index 54d67995d2..59308dd496 100644 --- a/src/Api/Vault/Models/Request/CipherBulkUpdateCollectionsRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherBulkUpdateCollectionsRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.Vault.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.Vault.Models.Request; public class CipherBulkUpdateCollectionsRequestModel { diff --git a/src/Api/Vault/Models/Request/CipherPartialRequestModel.cs b/src/Api/Vault/Models/Request/CipherPartialRequestModel.cs index 6232f4ecf6..02977ca1fe 100644 --- a/src/Api/Vault/Models/Request/CipherPartialRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherPartialRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Vault.Models.Request; diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 229d27e484..187fd13e30 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; diff --git a/src/Api/Vault/Models/Request/FolderRequestModel.cs b/src/Api/Vault/Models/Request/FolderRequestModel.cs index db9b65099f..27f34474be 100644 --- a/src/Api/Vault/Models/Request/FolderRequestModel.cs +++ b/src/Api/Vault/Models/Request/FolderRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; diff --git a/src/Api/Vault/Models/Response/AttachmentResponseModel.cs b/src/Api/Vault/Models/Response/AttachmentResponseModel.cs index f3c0261e98..4edebb539e 100644 --- a/src/Api/Vault/Models/Response/AttachmentResponseModel.cs +++ b/src/Api/Vault/Models/Response/AttachmentResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; diff --git a/src/Api/Vault/Models/Response/AttachmentUploadDataResponseModel.cs b/src/Api/Vault/Models/Response/AttachmentUploadDataResponseModel.cs index 9eff417769..bb735ace4b 100644 --- a/src/Api/Vault/Models/Response/AttachmentUploadDataResponseModel.cs +++ b/src/Api/Vault/Models/Response/AttachmentUploadDataResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Models.Api; namespace Bit.Api.Vault.Models.Response; diff --git a/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs index 4f2f7e86b2..b3082fc689 100644 --- a/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Vault.Authorization.Permissions; using Bit.Core.Vault.Models.Data; diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 240783837e..9d053f6697 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data.Organizations; diff --git a/src/Api/Vault/Models/Response/FolderResponseModel.cs b/src/Api/Vault/Models/Response/FolderResponseModel.cs index 72ba08cb3b..21c25b19fe 100644 --- a/src/Api/Vault/Models/Response/FolderResponseModel.cs +++ b/src/Api/Vault/Models/Response/FolderResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; using Bit.Core.Vault.Entities; namespace Bit.Api.Vault.Models.Response; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index b9da786567..dc34ffa46a 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Response.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Response; using Bit.Core.AdminConsole.Entities; diff --git a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs index 98a4bea591..fbd957afae 100644 --- a/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs +++ b/src/Core/Vault/Commands/CreateManyTaskNotificationsCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Commands.Interfaces; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Enums; diff --git a/src/Core/Vault/Entities/Cipher.cs b/src/Core/Vault/Entities/Cipher.cs index 6a3ce94cf1..8d8282d83c 100644 --- a/src/Core/Vault/Entities/Cipher.cs +++ b/src/Core/Vault/Entities/Cipher.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Models/Data/AttachmentResponseData.cs b/src/Core/Vault/Models/Data/AttachmentResponseData.cs index ecb1404912..c55d29053c 100644 --- a/src/Core/Vault/Models/Data/AttachmentResponseData.cs +++ b/src/Core/Vault/Models/Data/AttachmentResponseData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Vault.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Vault.Entities; namespace Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Models/Data/CipherAttachment.cs b/src/Core/Vault/Models/Data/CipherAttachment.cs index 6450efe632..1d4335f970 100644 --- a/src/Core/Vault/Models/Data/CipherAttachment.cs +++ b/src/Core/Vault/Models/Data/CipherAttachment.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; namespace Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Models/Data/CipherCardData.cs b/src/Core/Vault/Models/Data/CipherCardData.cs index 72a60176f2..a7d6e4c0db 100644 --- a/src/Core/Vault/Models/Data/CipherCardData.cs +++ b/src/Core/Vault/Models/Data/CipherCardData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public class CipherCardData : CipherData { diff --git a/src/Core/Vault/Models/Data/CipherData.cs b/src/Core/Vault/Models/Data/CipherData.cs index 459ee0d739..10e0315453 100644 --- a/src/Core/Vault/Models/Data/CipherData.cs +++ b/src/Core/Vault/Models/Data/CipherData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public abstract class CipherData { diff --git a/src/Core/Vault/Models/Data/CipherFieldData.cs b/src/Core/Vault/Models/Data/CipherFieldData.cs index b7969b11a2..303aa1da10 100644 --- a/src/Core/Vault/Models/Data/CipherFieldData.cs +++ b/src/Core/Vault/Models/Data/CipherFieldData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Vault.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Vault.Enums; namespace Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Models/Data/CipherIdentityData.cs b/src/Core/Vault/Models/Data/CipherIdentityData.cs index 9a8b2811ae..c4b251ba6f 100644 --- a/src/Core/Vault/Models/Data/CipherIdentityData.cs +++ b/src/Core/Vault/Models/Data/CipherIdentityData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public class CipherIdentityData : CipherData { diff --git a/src/Core/Vault/Models/Data/CipherLoginData.cs b/src/Core/Vault/Models/Data/CipherLoginData.cs index e2d1776abd..cdbe566c2f 100644 --- a/src/Core/Vault/Models/Data/CipherLoginData.cs +++ b/src/Core/Vault/Models/Data/CipherLoginData.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; namespace Bit.Core.Vault.Models.Data; diff --git a/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs b/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs index eefb7ec6ad..738e43a7cd 100644 --- a/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs +++ b/src/Core/Vault/Models/Data/CipherLoginFido2CredentialData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public class CipherLoginFido2CredentialData { diff --git a/src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs b/src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs index 3cac41f416..ef48674731 100644 --- a/src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs +++ b/src/Core/Vault/Models/Data/CipherPasswordHistoryData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public class CipherPasswordHistoryData { diff --git a/src/Core/Vault/Models/Data/CipherSSHKeyData.cs b/src/Core/Vault/Models/Data/CipherSSHKeyData.cs index 45c2cf6074..5138ce7d21 100644 --- a/src/Core/Vault/Models/Data/CipherSSHKeyData.cs +++ b/src/Core/Vault/Models/Data/CipherSSHKeyData.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; public class CipherSSHKeyData : CipherData { diff --git a/src/Core/Vault/Models/Data/UserCipherForTask.cs b/src/Core/Vault/Models/Data/UserCipherForTask.cs index 3ddaa141b1..27166e7242 100644 --- a/src/Core/Vault/Models/Data/UserCipherForTask.cs +++ b/src/Core/Vault/Models/Data/UserCipherForTask.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; /// /// Minimal data model that represents a User and the associated cipher for a security task. diff --git a/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs b/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs index 20e59ec4f7..a884163f42 100644 --- a/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs +++ b/src/Core/Vault/Models/Data/UserSecurityTaskCipher.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; /// /// Data model that represents a User and the associated cipher for a security task. diff --git a/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs b/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs index c8d2707db6..7385f77593 100644 --- a/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs +++ b/src/Core/Vault/Models/Data/UserSecurityTasksCount.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Vault.Models.Data; /// /// Data model that represents a User and the amount of actionable security tasks. diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index d3f8d20c90..dac535433c 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Vault.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; namespace Bit.Core.Vault.Services; diff --git a/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs b/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs index 89b152a645..d03a7e5fcf 100644 --- a/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs +++ b/src/Core/Vault/Services/Implementations/AzureAttachmentStorageService.cs @@ -1,4 +1,7 @@ -using Azure.Storage.Blobs; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Azure.Storage.Sas; using Bit.Core.Enums; diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 42221adf4b..51ed4b0ce7 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; diff --git a/src/Core/Vault/Services/NoopImplementations/NoopAttachmentStorageService.cs b/src/Core/Vault/Services/NoopImplementations/NoopAttachmentStorageService.cs index 6e6379d5b3..8014849d93 100644 --- a/src/Core/Vault/Services/NoopImplementations/NoopAttachmentStorageService.cs +++ b/src/Core/Vault/Services/NoopImplementations/NoopAttachmentStorageService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; diff --git a/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs b/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs index 8f60d81d90..0ac780ce08 100644 --- a/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs +++ b/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Vault.Entities; using Dapper; diff --git a/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs b/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs index 4428316de2..8fc2bec702 100644 --- a/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs +++ b/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Vault.Entities; using Dapper; diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 9c75295f3f..180a90fd41 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using System.Text.Json; using Bit.Core.Entities; using Bit.Core.KeyManagement.UserKey; diff --git a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs index a6f6f2ee22..63da064f88 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Vault.Entities; diff --git a/src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs b/src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs index 6655f98912..1cd2ce62fe 100644 --- a/src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs +++ b/src/Infrastructure.EntityFramework/Vault/Models/Cipher.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/Vault/Models/Folder.cs b/src/Infrastructure.EntityFramework/Vault/Models/Folder.cs index e27161384e..3485cf19a3 100644 --- a/src/Infrastructure.EntityFramework/Vault/Models/Folder.cs +++ b/src/Infrastructure.EntityFramework/Vault/Models/Folder.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs b/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs index 828c3bbc7d..2dbf37d0cd 100644 --- a/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs +++ b/src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Vault.Models; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index a560e8e107..11a74a8097 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; using Bit.Core.KeyManagement.UserKey; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs index 09ac256332..83fa442eb4 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs index c36c0d87c4..1038c208c2 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/UserSecurityTasksByCipherIdsQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.Vault.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; From 85b2a5bd94b870c27e492a0978c82c5156ed9398 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:46:24 -0400 Subject: [PATCH 030/326] Add `#nullable disable` to billing code (#6054) --- .../Providers/Models/ProviderClientInvoiceReportRow.cs | 5 ++++- .../Billing/Providers/Services/ProviderBillingService.cs | 5 ++++- .../Billing/Controllers/ProcessStripeEventsController.cs | 5 ++++- src/Admin/Billing/Models/MigrateProvidersRequestModel.cs | 5 ++++- .../Billing/Models/ProcessStripeEvents/EventsFormModel.cs | 5 ++++- .../Billing/Models/ProcessStripeEvents/EventsRequestBody.cs | 5 ++++- .../Billing/Models/ProcessStripeEvents/EventsResponseBody.cs | 5 ++++- src/Admin/Controllers/ToolsController.cs | 5 ++++- src/Admin/Views/Tools/ChargeBraintree.cshtml | 2 +- src/Api/Billing/Controllers/BaseProviderController.cs | 5 ++++- src/Api/Billing/Controllers/InvoicesController.cs | 5 ++++- .../Controllers/OrganizationSponsorshipsController.cs | 5 ++++- src/Api/Billing/Controllers/OrganizationsController.cs | 5 ++++- src/Api/Billing/Controllers/ProviderBillingController.cs | 5 ++++- .../Models/Requests/AddExistingOrganizationRequestBody.cs | 5 ++++- .../Models/Requests/CreateClientOrganizationRequestBody.cs | 5 ++++- src/Api/Billing/Models/Requests/KeyPairRequestBody.cs | 5 ++++- .../Billing/Models/Requests/SetupBusinessUnitRequestBody.cs | 5 ++++- src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs | 5 ++++- .../Models/Requests/TokenizedPaymentSourceRequestBody.cs | 5 ++++- .../Models/Requests/UpdateClientOrganizationRequestBody.cs | 5 ++++- .../Models/Requests/UpdatePaymentMethodRequestBody.cs | 5 ++++- .../Billing/Models/Requests/VerifyBankAccountRequestBody.cs | 5 ++++- .../Billing/Models/Responses/BillingHistoryResponseModel.cs | 5 ++++- .../Billing/Models/Responses/BillingPaymentResponseModel.cs | 5 ++++- src/Api/Billing/Models/Responses/BillingResponseModel.cs | 5 ++++- src/Api/Billing/Public/Controllers/OrganizationController.cs | 5 ++++- .../Request/OrganizationSubscriptionUpdateRequestModel.cs | 5 ++++- .../Response/OrganizationSubscriptionDetailsResponseModel.cs | 5 ++++- .../SelfHosted/SelfHostedOrganizationLicensesController.cs | 5 ++++- src/Billing/BillingSettings.cs | 5 ++++- src/Billing/Controllers/AppleController.cs | 5 ++++- src/Billing/Controllers/BitPayController.cs | 5 ++++- src/Billing/Controllers/FreshdeskController.cs | 5 ++++- src/Billing/Controllers/FreshsalesController.cs | 5 ++++- src/Billing/Controllers/PayPalController.cs | 5 ++++- src/Billing/Controllers/RecoveryController.cs | 5 ++++- src/Billing/Controllers/StripeController.cs | 5 ++++- src/Billing/Jobs/SubscriptionCancellationJob.cs | 5 ++++- src/Billing/Models/BitPayEventModel.cs | 5 ++++- src/Billing/Models/FreshdeskViewTicketModel.cs | 5 ++++- src/Billing/Models/FreshdeskWebhookModel.cs | 5 ++++- src/Billing/Models/LoginModel.cs | 5 ++++- src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs | 5 ++++- src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs | 5 ++++- src/Billing/Models/PayPalIPNTransactionModel.cs | 5 ++++- src/Billing/Models/Recovery/EventsRequestBody.cs | 5 ++++- src/Billing/Models/Recovery/EventsResponseBody.cs | 5 ++++- src/Billing/Models/StripeWebhookDeliveryContainer.cs | 5 ++++- src/Billing/Services/IStripeEventService.cs | 5 ++++- src/Billing/Services/IStripeFacade.cs | 5 ++++- .../Services/Implementations/PaymentMethodAttachedHandler.cs | 5 ++++- src/Billing/Services/Implementations/ProviderEventService.cs | 5 ++++- src/Billing/Services/Implementations/StripeEventService.cs | 5 ++++- .../Services/Implementations/StripeEventUtilityService.cs | 5 ++++- src/Billing/Services/Implementations/StripeFacade.cs | 5 ++++- .../Services/Implementations/UpcomingInvoiceHandler.cs | 5 ++++- src/Billing/Startup.cs | 5 ++++- src/Core/Billing/BillingException.cs | 5 ++++- .../Caches/Implementations/SetupIntentDistributedCache.cs | 5 ++++- src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs | 5 ++++- .../Implementations/OrganizationLicenseClaimsFactory.cs | 5 ++++- .../Services/Implementations/UserLicenseClaimsFactory.cs | 5 ++++- .../Accounts/TrialSendVerificationEmailRequestModel.cs | 5 ++++- src/Core/Billing/Models/BillingHistoryInfo.cs | 5 ++++- src/Core/Billing/Models/BillingInfo.cs | 5 ++++- .../Models/Business/SponsorOrganizationSubscriptionUpdate.cs | 5 ++++- src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs | 5 ++++- src/Core/Billing/Models/OffboardingSurveyResponse.cs | 5 ++++- src/Core/Billing/Models/PaymentMethod.cs | 5 ++++- src/Core/Billing/Models/PaymentSource.cs | 5 ++++- src/Core/Billing/Models/StaticStore/Plan.cs | 5 ++++- src/Core/Billing/Models/StaticStore/SponsoredPlan.cs | 5 ++++- .../Providers/Migration/Models/ClientMigrationTracker.cs | 5 ++++- .../Providers/Migration/Models/ProviderMigrationResult.cs | 5 ++++- .../Providers/Migration/Models/ProviderMigrationTracker.cs | 5 ++++- .../Implementations/MigrationTrackerDistributedCache.cs | 5 ++++- .../Services/Implementations/OrganizationMigrator.cs | 5 ++++- .../Migration/Services/Implementations/ProviderMigrator.cs | 5 ++++- .../Billing/Providers/Services/IProviderBillingService.cs | 5 ++++- src/Core/Billing/Services/ISubscriberService.cs | 5 ++++- .../Services/Implementations/PremiumUserBillingService.cs | 5 ++++- .../Billing/Services/Implementations/SubscriberService.cs | 5 ++++- src/Core/Billing/Tax/Models/TaxIdType.cs | 5 ++++- .../Tax/Requests/PreviewIndividualInvoiceRequestModel.cs | 5 ++++- .../Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs | 5 ++++- src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs | 5 ++++- src/Core/Billing/Tax/Services/Implementations/TaxService.cs | 5 ++++- src/Core/Billing/Utilities.cs | 5 ++++- .../SelfHostedOrganizationLicenseRequestModel.cs | 5 ++++- src/Core/Models/Business/OrganizationLicense.cs | 5 ++++- .../Models/Mail/Billing/BusinessUnitConversionInviteModel.cs | 5 ++++- .../Cloud/CloudGetOrganizationLicenseQuery.cs | 5 ++++- .../SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs | 5 ++++- .../ClientOrganizationMigrationRecordRepository.cs | 5 ++++- .../Repositories/OrganizationInstallationRepository.cs | 5 ++++- .../Billing/Models/OrganizationInstallation.cs | 5 ++++- .../Billing/Models/ProviderInvoiceItem.cs | 5 ++++- .../Billing/Models/ProviderPlan.cs | 5 ++++- .../ClientOrganizationMigrationRecordRepository.cs | 5 ++++- .../Repositories/OrganizationInstallationRepository.cs | 5 ++++- 101 files changed, 401 insertions(+), 101 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs index eea40577ad..5fff607f79 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using Bit.Core.Billing.Providers.Entities; using CsvHelper.Configuration.Attributes; 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 2b337fb4bb..e02b52cd46 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Core; using Bit.Core.AdminConsole.Entities; diff --git a/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs index 1a3f56a183..80ad7fef4e 100644 --- a/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs +++ b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Admin.Billing.Models.ProcessStripeEvents; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs index fe1d88e224..273f934eba 100644 --- a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs +++ b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// 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; diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs index 5ead00e263..b78d8cc89e 100644 --- a/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Bit.Admin.Billing.Models.ProcessStripeEvents; diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs index 05a2444605..ea9b9c1045 100644 --- a/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; namespace Bit.Admin.Billing.Models.ProcessStripeEvents; diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs index 84eeb35d29..5c55b3a8b4 100644 --- a/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; namespace Bit.Admin.Billing.Models.ProcessStripeEvents; diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index eaf3de4be5..a640fe352f 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -1,4 +1,7 @@ -using System.Text; +// 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; diff --git a/src/Admin/Views/Tools/ChargeBraintree.cshtml b/src/Admin/Views/Tools/ChargeBraintree.cshtml index aaf3bbf167..0c661a8ee4 100644 --- a/src/Admin/Views/Tools/ChargeBraintree.cshtml +++ b/src/Admin/Views/Tools/ChargeBraintree.cshtml @@ -8,7 +8,7 @@ @if(!string.IsNullOrWhiteSpace(Model.TransactionId)) { diff --git a/src/Api/Billing/Controllers/BaseProviderController.cs b/src/Api/Billing/Controllers/BaseProviderController.cs index 038abfaa9e..782bffbc70 100644 --- a/src/Api/Billing/Controllers/BaseProviderController.cs +++ b/src/Api/Billing/Controllers/BaseProviderController.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities.Provider; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Extensions; using Bit.Core.Context; diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs index 5a1d732f42..30ea975e09 100644 --- a/src/Api/Billing/Controllers/InvoicesController.cs +++ b/src/Api/Billing/Controllers/InvoicesController.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Tax.Requests; using Bit.Core.Context; using Bit.Core.Repositories; diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index c45b34422c..2d05595b2d 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Request.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Core.AdminConsole.Enums; diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index c8a3c20c91..a74974ab46 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request; diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 1309b2df6d..80b145a2e0 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Billing.Models.Requests; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core; diff --git a/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs index c2add17793..f23ce266c8 100644 --- a/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/AddExistingOrganizationRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs index 95836151d6..243126f7ac 100644 --- a/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/CreateClientOrganizationRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Utilities; using Bit.Core.Billing.Enums; diff --git a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs index b4f2c00f4f..2fec3bd61d 100644 --- a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs +++ b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/SetupBusinessUnitRequestBody.cs b/src/Api/Billing/Models/Requests/SetupBusinessUnitRequestBody.cs index c4b87a01f5..bbc6a9acda 100644 --- a/src/Api/Billing/Models/Requests/SetupBusinessUnitRequestBody.cs +++ b/src/Api/Billing/Models/Requests/SetupBusinessUnitRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index edc45ce483..a1b754a9dc 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs b/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs index 4f087913b9..b469ce2576 100644 --- a/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Utilities; using Bit.Core.Billing.Models; using Bit.Core.Enums; diff --git a/src/Api/Billing/Models/Requests/UpdateClientOrganizationRequestBody.cs b/src/Api/Billing/Models/Requests/UpdateClientOrganizationRequestBody.cs index 6ed1083b42..7c393e342a 100644 --- a/src/Api/Billing/Models/Requests/UpdateClientOrganizationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/UpdateClientOrganizationRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs b/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs index cdc9a08851..05ab1e34c9 100644 --- a/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs +++ b/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs index 3e97d07a90..e248d55dde 100644 --- a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs +++ b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Responses/BillingHistoryResponseModel.cs b/src/Api/Billing/Models/Responses/BillingHistoryResponseModel.cs index 0a4ebdb8dd..9f68fe41a4 100644 --- a/src/Api/Billing/Models/Responses/BillingHistoryResponseModel.cs +++ b/src/Api/Billing/Models/Responses/BillingHistoryResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs b/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs index 5c43522aca..f305e41c4f 100644 --- a/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs +++ b/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; using Bit.Core.Models.Api; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Billing/Models/Responses/BillingResponseModel.cs b/src/Api/Billing/Models/Responses/BillingResponseModel.cs index 172f784b50..67f4c98f9d 100644 --- a/src/Api/Billing/Models/Responses/BillingResponseModel.cs +++ b/src/Api/Billing/Models/Responses/BillingResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; using Bit.Core.Enums; using Bit.Core.Models.Api; diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs index b0a0537ed8..79033ba31e 100644 --- a/src/Api/Billing/Public/Controllers/OrganizationController.cs +++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.Billing.Public.Models; using Bit.Api.Models.Public.Response; using Bit.Core.Billing.Pricing; diff --git a/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs index 5c75db5924..4ccbdb04e8 100644 --- a/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs +++ b/src/Api/Billing/Public/Models/Request/OrganizationSubscriptionUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; diff --git a/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs b/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs index 09aa7decc1..0a3b7b1421 100644 --- a/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs +++ b/src/Api/Billing/Public/Models/Response/OrganizationSubscriptionDetailsResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Public.Models; diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index ed501c41da..a0cc64b9bf 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Response.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Organizations; using Bit.Api.Utilities; diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index ffe73808d4..9189fe6cd0 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -1,4 +1,7 @@ -namespace Bit.Billing; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Billing; public class BillingSettings { diff --git a/src/Billing/Controllers/AppleController.cs b/src/Billing/Controllers/AppleController.cs index 5c231de8ed..c08f1cfa61 100644 --- a/src/Billing/Controllers/AppleController.cs +++ b/src/Billing/Controllers/AppleController.cs @@ -1,4 +1,7 @@ -using System.Text; +// 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.Core.Utilities; using Microsoft.AspNetCore.Mvc; diff --git a/src/Billing/Controllers/BitPayController.cs b/src/Billing/Controllers/BitPayController.cs index a8d1742fcb..111ffabc2b 100644 --- a/src/Billing/Controllers/BitPayController.cs +++ b/src/Billing/Controllers/BitPayController.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using Bit.Billing.Constants; using Bit.Billing.Models; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 1fb0fb7ac7..40c9c39d95 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Net.Http.Headers; using System.Reflection; using System.Text; diff --git a/src/Billing/Controllers/FreshsalesController.cs b/src/Billing/Controllers/FreshsalesController.cs index 0182011d7a..be5a9ddb16 100644 --- a/src/Billing/Controllers/FreshsalesController.cs +++ b/src/Billing/Controllers/FreshsalesController.cs @@ -1,4 +1,7 @@ -using System.Net.Http.Headers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net.Http.Headers; using System.Text.Json.Serialization; using Bit.Core.Billing.Enums; using Bit.Core.Repositories; diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 36987c6e44..8039680fd5 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -1,4 +1,7 @@ -using System.Text; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text; using Bit.Billing.Models; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Services; diff --git a/src/Billing/Controllers/RecoveryController.cs b/src/Billing/Controllers/RecoveryController.cs index bada1e826d..3f3dc4e650 100644 --- a/src/Billing/Controllers/RecoveryController.cs +++ b/src/Billing/Controllers/RecoveryController.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Models.Recovery; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Models.Recovery; using Bit.Billing.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 5ea2733a18..b60e0c56e4 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Models; using Bit.Billing.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; diff --git a/src/Billing/Jobs/SubscriptionCancellationJob.cs b/src/Billing/Jobs/SubscriptionCancellationJob.cs index b59bb10eaf..69b7bc876d 100644 --- a/src/Billing/Jobs/SubscriptionCancellationJob.cs +++ b/src/Billing/Jobs/SubscriptionCancellationJob.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Services; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Services; using Bit.Core.Repositories; using Quartz; using Stripe; diff --git a/src/Billing/Models/BitPayEventModel.cs b/src/Billing/Models/BitPayEventModel.cs index e16391317a..008d4942a6 100644 --- a/src/Billing/Models/BitPayEventModel.cs +++ b/src/Billing/Models/BitPayEventModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Billing.Models; public class BitPayEventModel { diff --git a/src/Billing/Models/FreshdeskViewTicketModel.cs b/src/Billing/Models/FreshdeskViewTicketModel.cs index 2aa6eff94d..e4d485072c 100644 --- a/src/Billing/Models/FreshdeskViewTicketModel.cs +++ b/src/Billing/Models/FreshdeskViewTicketModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Billing.Models; using System; using System.Collections.Generic; diff --git a/src/Billing/Models/FreshdeskWebhookModel.cs b/src/Billing/Models/FreshdeskWebhookModel.cs index e9fe8e026a..19c94d5eba 100644 --- a/src/Billing/Models/FreshdeskWebhookModel.cs +++ b/src/Billing/Models/FreshdeskWebhookModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// 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; diff --git a/src/Billing/Models/LoginModel.cs b/src/Billing/Models/LoginModel.cs index 5fe04ad454..f758dc8590 100644 --- a/src/Billing/Models/LoginModel.cs +++ b/src/Billing/Models/LoginModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Billing.Models; diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index e7bd29b2f5..c21ea9fc19 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -1,4 +1,7 @@ - +// 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; diff --git a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs index e85ee9a674..5f67cd51d2 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// 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; diff --git a/src/Billing/Models/PayPalIPNTransactionModel.cs b/src/Billing/Models/PayPalIPNTransactionModel.cs index 6fd0dfa0c4..34db5fdd04 100644 --- a/src/Billing/Models/PayPalIPNTransactionModel.cs +++ b/src/Billing/Models/PayPalIPNTransactionModel.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Runtime.InteropServices; using System.Web; diff --git a/src/Billing/Models/Recovery/EventsRequestBody.cs b/src/Billing/Models/Recovery/EventsRequestBody.cs index a40f8c9655..f3293cb48a 100644 --- a/src/Billing/Models/Recovery/EventsRequestBody.cs +++ b/src/Billing/Models/Recovery/EventsRequestBody.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// 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.Recovery; diff --git a/src/Billing/Models/Recovery/EventsResponseBody.cs b/src/Billing/Models/Recovery/EventsResponseBody.cs index a0c7f087b7..a706734133 100644 --- a/src/Billing/Models/Recovery/EventsResponseBody.cs +++ b/src/Billing/Models/Recovery/EventsResponseBody.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// 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.Recovery; diff --git a/src/Billing/Models/StripeWebhookDeliveryContainer.cs b/src/Billing/Models/StripeWebhookDeliveryContainer.cs index 6588aa7d13..9d566146fb 100644 --- a/src/Billing/Models/StripeWebhookDeliveryContainer.cs +++ b/src/Billing/Models/StripeWebhookDeliveryContainer.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// 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; diff --git a/src/Billing/Services/IStripeEventService.cs b/src/Billing/Services/IStripeEventService.cs index 6e2239cf98..bf242905ee 100644 --- a/src/Billing/Services/IStripeEventService.cs +++ b/src/Billing/Services/IStripeEventService.cs @@ -1,4 +1,7 @@ -using Stripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Stripe; namespace Bit.Billing.Services; diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index e53d901083..6886250a33 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -1,4 +1,7 @@ -using Stripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Stripe; namespace Bit.Billing.Services; diff --git a/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs b/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs index 97bb29c35d..ee5a50cc98 100644 --- a/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs +++ b/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Constants; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Constants; using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 1f6ef741df..12716c5aa2 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Constants; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; diff --git a/src/Billing/Services/Implementations/StripeEventService.cs b/src/Billing/Services/Implementations/StripeEventService.cs index 8d947e0ccb..7e2984e423 100644 --- a/src/Billing/Services/Implementations/StripeEventService.cs +++ b/src/Billing/Services/Implementations/StripeEventService.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Constants; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Constants; using Bit.Core.Settings; using Stripe; diff --git a/src/Billing/Services/Implementations/StripeEventUtilityService.cs b/src/Billing/Services/Implementations/StripeEventUtilityService.cs index 48e81dee61..4c96bf977d 100644 --- a/src/Billing/Services/Implementations/StripeEventUtilityService.cs +++ b/src/Billing/Services/Implementations/StripeEventUtilityService.cs @@ -1,4 +1,7 @@ -using Bit.Billing.Constants; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Billing.Constants; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index 191f84a343..70144d8cd3 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -1,4 +1,7 @@ -using Stripe; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Stripe; namespace Bit.Billing.Services.Implementations; diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index e31d1dceb7..323eaf5155 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,4 +1,7 @@ -using Bit.Core; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index afb01f4801..24b5372ba1 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Net.Http.Headers; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; diff --git a/src/Core/Billing/BillingException.cs b/src/Core/Billing/BillingException.cs index c2b1b9f457..1203a15f7b 100644 --- a/src/Core/Billing/BillingException.cs +++ b/src/Core/Billing/BillingException.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Billing; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Billing; public class BillingException( string response = null, diff --git a/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs b/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs index ceb512a0e3..432a778762 100644 --- a/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs +++ b/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Caching.Distributed; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Billing.Caches.Implementations; diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index 184d8dad23..9ac1ace156 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -1,4 +1,7 @@ -using System.Security.Claims; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index b3f2ab4ec9..678ac7f97e 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; diff --git a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs index 2aaa5efdc1..5ad1a4a294 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs @@ -1,4 +1,7 @@ -using System.Globalization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Globalization; using System.Security.Claims; using Bit.Core.Billing.Licenses.Models; using Bit.Core.Entities; diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs index b31da9efbc..3392594f06 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs +++ b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Api.Request.Accounts; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Billing.Enums; namespace Bit.Core.Billing.Models.Api.Requests.Accounts; diff --git a/src/Core/Billing/Models/BillingHistoryInfo.cs b/src/Core/Billing/Models/BillingHistoryInfo.cs index 03017b9b4d..3114e22fdf 100644 --- a/src/Core/Billing/Models/BillingHistoryInfo.cs +++ b/src/Core/Billing/Models/BillingHistoryInfo.cs @@ -1,4 +1,7 @@ -using Bit.Core.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Entities; using Bit.Core.Enums; using Stripe; diff --git a/src/Core/Billing/Models/BillingInfo.cs b/src/Core/Billing/Models/BillingInfo.cs index 9bdc042570..5b7f2484be 100644 --- a/src/Core/Billing/Models/BillingInfo.cs +++ b/src/Core/Billing/Models/BillingInfo.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Stripe; namespace Bit.Core.Billing.Models; diff --git a/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs b/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs index 830105e373..4bcc7ed699 100644 --- a/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs +++ b/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Business; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Business; using Stripe; namespace Bit.Core.Billing.Models.Business; diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index b97390dcc9..019edccd04 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.Models.Mail; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; using Bit.Core.Enums; diff --git a/src/Core/Billing/Models/OffboardingSurveyResponse.cs b/src/Core/Billing/Models/OffboardingSurveyResponse.cs index cd966f40cc..0d55dcdc56 100644 --- a/src/Core/Billing/Models/OffboardingSurveyResponse.cs +++ b/src/Core/Billing/Models/OffboardingSurveyResponse.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Billing.Models; public class OffboardingSurveyResponse { diff --git a/src/Core/Billing/Models/PaymentMethod.cs b/src/Core/Billing/Models/PaymentMethod.cs index 2b8c59fa05..14ee79b714 100644 --- a/src/Core/Billing/Models/PaymentMethod.cs +++ b/src/Core/Billing/Models/PaymentMethod.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Tax.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Models; diff --git a/src/Core/Billing/Models/PaymentSource.cs b/src/Core/Billing/Models/PaymentSource.cs index 44bbddc66b..130b0f71c4 100644 --- a/src/Core/Billing/Models/PaymentSource.cs +++ b/src/Core/Billing/Models/PaymentSource.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Extensions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Extensions; using Bit.Core.Enums; namespace Bit.Core.Billing.Models; diff --git a/src/Core/Billing/Models/StaticStore/Plan.cs b/src/Core/Billing/Models/StaticStore/Plan.cs index d710594f46..540ea76582 100644 --- a/src/Core/Billing/Models/StaticStore/Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plan.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Enums; namespace Bit.Core.Models.StaticStore; diff --git a/src/Core/Billing/Models/StaticStore/SponsoredPlan.cs b/src/Core/Billing/Models/StaticStore/SponsoredPlan.cs index d0d98159a8..840a652225 100644 --- a/src/Core/Billing/Models/StaticStore/SponsoredPlan.cs +++ b/src/Core/Billing/Models/StaticStore/SponsoredPlan.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; diff --git a/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs index ae0c28de86..65fd7726f8 100644 --- a/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Billing.Providers.Migration.Models; +// 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 { diff --git a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs index 6f3c3be11d..78a2631999 100644 --- a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Providers.Entities; +// 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; diff --git a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs index f4708d4cbd..ba39feab2d 100644 --- a/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Billing.Providers.Migration.Models; +// 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 { diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs index ea7d118cfa..1f38b0d111 100644 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// 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; diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs index 3b874579e5..3de49838af 100644 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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; diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs index 3a0b579dcf..3a33f96dab 100644 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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; diff --git a/src/Core/Billing/Providers/Services/IProviderBillingService.cs b/src/Core/Billing/Providers/Services/IProviderBillingService.cs index b634f1a81c..518fa1ba98 100644 --- a/src/Core/Billing/Providers/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Providers/Services/IProviderBillingService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Billing.Enums; using Bit.Core.Billing.Models; diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index 6910948436..ef43bde010 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 7496157aaa..5b1b717c20 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Caches; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 796f700e9f..7a0e78a6dc 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Billing.Caches; using Bit.Core.Billing.Constants; diff --git a/src/Core/Billing/Tax/Models/TaxIdType.cs b/src/Core/Billing/Tax/Models/TaxIdType.cs index 6f8cfdde99..005b1eb6a6 100644 --- a/src/Core/Billing/Tax/Models/TaxIdType.cs +++ b/src/Core/Billing/Tax/Models/TaxIdType.cs @@ -1,4 +1,7 @@ -using System.Text.RegularExpressions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.RegularExpressions; namespace Bit.Core.Billing.Tax.Models; diff --git a/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs index 340f07b56c..db5ba190bd 100644 --- a/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Billing.Tax.Requests; diff --git a/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs index bfb47e7b2c..f0bc368f07 100644 --- a/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Enums; using Bit.Core.Enums; diff --git a/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs index 13d4870ac5..cd1046f480 100644 --- a/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Billing.Tax.Requests; diff --git a/src/Core/Billing/Tax/Services/Implementations/TaxService.cs b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs index 204c997335..55a8ab1c50 100644 --- a/src/Core/Billing/Tax/Services/Implementations/TaxService.cs +++ b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs @@ -1,4 +1,7 @@ -using System.Text.RegularExpressions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.RegularExpressions; using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Tax.Services.Implementations; diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index ebb7b0e525..2ee6b75664 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -1,4 +1,7 @@ -using Bit.Core.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Models; using Bit.Core.Services; using Stripe; diff --git a/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs index 365d88877e..a010f828de 100644 --- a/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs +++ b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Api.OrganizationLicenses; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Api.OrganizationLicenses; public class SelfHostedOrganizationLicenseRequestModel { diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index e8c04b1277..c40f5fd899 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -1,4 +1,7 @@ -using System.Reflection; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Reflection; using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; diff --git a/src/Core/Models/Mail/Billing/BusinessUnitConversionInviteModel.cs b/src/Core/Models/Mail/Billing/BusinessUnitConversionInviteModel.cs index 328d37058b..ab0d2955e2 100644 --- a/src/Core/Models/Mail/Billing/BusinessUnitConversionInviteModel.cs +++ b/src/Core/Models/Mail/Billing/BusinessUnitConversionInviteModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Models.Mail.Billing; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Models.Mail.Billing; public class BusinessUnitConversionInviteModel : BaseMailModel { diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index 44edde1495..99a8e68380 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs index 89ea53fc20..7c78abe480 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index e43eb9a71f..500b76a309 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs index f73eefb793..a3e2063312 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Repositories; using Bit.Core.Settings; diff --git a/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs index c59a2accba..da8ee8b1b6 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.Platform; diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs index 1bea786f21..2fdd27868e 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs index c9ba4c813e..7bdb7298e4 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index 4a9a82c9dc..d6363155f0 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs index 566c52332e..9656b1073d 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; From 37cdefbf8901137a2bbe90624e3f36d2a1f95a7e Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:48:02 -0400 Subject: [PATCH 031/326] Add `#nullable disable` to DIRT code (#6059) --- src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs | 5 ++++- src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs | 5 ++++- src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs | 5 ++++- src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs | 5 ++++- .../Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs | 5 ++++- .../ReportFeatures/Requests/AddOrganizationReportRequest.cs | 5 ++++- .../Requests/AddPasswordHealthReportApplicationRequest.cs | 5 ++++- .../ReportFeatures/Requests/DropOrganizationReportRequest.cs | 5 ++++- .../Requests/DropPasswordHealthReportApplicationRequest.cs | 5 ++++- .../Dirt/OrganizationReportRepository.cs | 5 ++++- 10 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs index c1949ffb24..326a7c61cb 100644 --- a/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs +++ b/src/Core/Dirt/Models/Data/MemberAccessCipherDetails.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Models.Data; public class MemberAccessDetails { diff --git a/src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs b/src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs index a99b6e2088..7b54822a1e 100644 --- a/src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs +++ b/src/Core/Dirt/Models/Data/MemberAccessReportDetail.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.Models.Data; public class MemberAccessReportDetail { diff --git a/src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs b/src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs index a1f0bd81fd..a68e920e66 100644 --- a/src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs +++ b/src/Core/Dirt/Models/Data/OrganizationMemberBaseDetail.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.Models.Data; public class OrganizationMemberBaseDetail { diff --git a/src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs b/src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs index 1ea805edf1..acc468eb11 100644 --- a/src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs +++ b/src/Core/Dirt/Models/Data/RiskInsightsReportDetail.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.Models.Data; public class RiskInsightsReportDetail { diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs index 21dbfc77a4..33acd73d14 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Dirt.Reports.Models.Data; using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs index ca892cddde..f5a3d581f2 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; public class AddOrganizationReportRequest { diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs index c4e646fcd7..884c7ea40a 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; public class AddPasswordHealthReportApplicationRequest { diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs index cc889fe351..04dc4b43a2 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropOrganizationReportRequest.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; public class DropOrganizationReportRequest { diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs index 544b9a51d5..3fc09af574 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs @@ -1,4 +1,7 @@ -namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; public class DropPasswordHealthReportApplicationRequest { diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs index 7a5fe1c8c2..2ce17a9983 100644 --- a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs @@ -1,4 +1,7 @@ -using System.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Data; using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; From 3d09db8e31f315d2c5820cc01d09ddbe1e5fe3dc Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:29:09 -0400 Subject: [PATCH 032/326] Add `#nullable disable` to KM code (#6056) --- .../Models/Requests/SetKeyConnectorKeyRequestModel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs b/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs index bac42bc302..9f52a97383 100644 --- a/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/SetKeyConnectorKeyRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; From b4c9133d124a951fb773b7ec9dbde8280057d397 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:59:44 -0400 Subject: [PATCH 033/326] feat(otp): Revert [PM-18612] Consolidate all email OTP to use 6 digits This reverts commit 737f549f8297709b9de487bf9b289e2584dd2329. --- .../Identity/TokenProviders/EmailTokenProvider.cs | 5 +---- .../TokenProviders/EmailTwoFactorTokenProvider.cs | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index 9481710390..be94124c03 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -7,9 +7,6 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; -/// -/// Generates and validates tokens for email OTPs. -/// public class EmailTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "EmailToken_{0}_{1}_{2}"; @@ -28,7 +25,7 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider }; } - public int TokenLength { get; protected set; } = 6; + public int TokenLength { get; protected set; } = 8; public bool TokenAlpha { get; protected set; } = false; public bool TokenNumeric { get; protected set; } = true; diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 49a000a2bf..c4b4c1d2ca 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -10,18 +10,17 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; -/// -/// Generates tokens for email two-factor authentication. -/// It inherits from the EmailTokenProvider class, which manages the persistence and validation of tokens, -/// and adds additional validation to ensure that 2FA is enabled for the user. -/// public class EmailTwoFactorTokenProvider : EmailTokenProvider { public EmailTwoFactorTokenProvider( [FromKeyedServices("persistent")] IDistributedCache distributedCache) : base(distributedCache) - { } + { + TokenAlpha = false; + TokenNumeric = true; + TokenLength = 6; + } public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { From 8fdd26bf1c0e788e6113efab60219c5220242c81 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:02:15 -0400 Subject: [PATCH 034/326] Add `#nullable disable` to tools code (#6058) --- .../Tools/Authorization/VaultExportAuthorizationHandler.cs | 5 ++++- src/Api/Tools/Controllers/ImportCiphersController.cs | 5 ++++- src/Api/Tools/Controllers/SendsController.cs | 5 ++++- .../Models/Request/Accounts/ImportCiphersRequestModel.cs | 5 ++++- .../Organizations/ImportOrganizationCiphersRequestModel.cs | 5 ++++- src/Api/Tools/Models/Request/SendAccessRequestModel.cs | 5 ++++- src/Api/Tools/Models/Request/SendRequestModel.cs | 5 ++++- .../Tools/Models/Response/OrganizationExportResponseModel.cs | 5 ++++- src/Api/Tools/Models/Response/SendAccessResponseModel.cs | 5 ++++- .../Models/Response/SendFileDownloadDataResponseModel.cs | 5 ++++- .../Tools/Models/Response/SendFileUploadDataResponseModel.cs | 5 ++++- src/Api/Tools/Models/Response/SendResponseModel.cs | 5 ++++- src/Api/Tools/Models/SendFileModel.cs | 5 ++++- src/Api/Tools/Models/SendTextModel.cs | 5 ++++- src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs | 5 ++++- src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs | 5 ++++- .../Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs | 5 ++++- .../SendFeatures/Services/AzureSendFileStorageService.cs | 5 ++++- .../Tools/SendFeatures/Services/LocalSendStorageService.cs | 5 ++++- .../Tools/SendFeatures/Services/SendValidationService.cs | 5 ++++- .../NoopImplementations/NoopSendFileStorageService.cs | 5 ++++- src/Infrastructure.EntityFramework/Tools/Models/Send.cs | 5 ++++- 22 files changed, 88 insertions(+), 22 deletions(-) diff --git a/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs b/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs index 337a0dc1e5..8968ecfee8 100644 --- a/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs +++ b/src/Api/Tools/Authorization/VaultExportAuthorizationHandler.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Context; using Bit.Core.Enums; using Microsoft.AspNetCore.Authorization; diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 817105c74b..0f29a9aee3 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Tools.Models.Request.Accounts; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Tools.Models.Request.Accounts; using Bit.Api.Tools.Models.Request.Organizations; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.Context; diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index a51ec942cf..43239b3995 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Messaging.EventGrid; using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Request; diff --git a/src/Api/Tools/Models/Request/Accounts/ImportCiphersRequestModel.cs b/src/Api/Tools/Models/Request/Accounts/ImportCiphersRequestModel.cs index 354d73ad04..8330e4fc54 100644 --- a/src/Api/Tools/Models/Request/Accounts/ImportCiphersRequestModel.cs +++ b/src/Api/Tools/Models/Request/Accounts/ImportCiphersRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Vault.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Vault.Models.Request; namespace Bit.Api.Tools.Models.Request.Accounts; diff --git a/src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs b/src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs index 8c88be136a..45f8dfdffd 100644 --- a/src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs +++ b/src/Api/Tools/Models/Request/Organizations/ImportOrganizationCiphersRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Request; using Bit.Api.Vault.Models.Request; namespace Bit.Api.Tools.Models.Request.Organizations; diff --git a/src/Api/Tools/Models/Request/SendAccessRequestModel.cs b/src/Api/Tools/Models/Request/SendAccessRequestModel.cs index c29577c2d0..15745ac855 100644 --- a/src/Api/Tools/Models/Request/SendAccessRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendAccessRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Tools.Models.Request; diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index 5b3fd7ba31..a38257db60 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.Exceptions; using Bit.Core.Tools.Entities; diff --git a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs index 5fd7e821cf..48fb96807e 100644 --- a/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs +++ b/src/Api/Tools/Models/Response/OrganizationExportResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; using Bit.Core.Entities; using Bit.Core.Models.Api; diff --git a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs index a3bb0f8bc0..b544862fcd 100644 --- a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Models.Api; using Bit.Core.Settings; using Bit.Core.Tools.Entities; diff --git a/src/Api/Tools/Models/Response/SendFileDownloadDataResponseModel.cs b/src/Api/Tools/Models/Response/SendFileDownloadDataResponseModel.cs index 47d5d3a840..8e20062301 100644 --- a/src/Api/Tools/Models/Response/SendFileDownloadDataResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendFileDownloadDataResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Api; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Api; namespace Bit.Api.Tools.Models.Response; diff --git a/src/Api/Tools/Models/Response/SendFileUploadDataResponseModel.cs b/src/Api/Tools/Models/Response/SendFileUploadDataResponseModel.cs index aee80de220..4f263b7e9c 100644 --- a/src/Api/Tools/Models/Response/SendFileUploadDataResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendFileUploadDataResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Models.Api; namespace Bit.Api.Tools.Models.Response; diff --git a/src/Api/Tools/Models/Response/SendResponseModel.cs b/src/Api/Tools/Models/Response/SendResponseModel.cs index 2ea217fd67..17a70cd2db 100644 --- a/src/Api/Tools/Models/Response/SendResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Models.Api; using Bit.Core.Settings; using Bit.Core.Tools.Entities; diff --git a/src/Api/Tools/Models/SendFileModel.cs b/src/Api/Tools/Models/SendFileModel.cs index 4af5b6ed6c..88deef4b13 100644 --- a/src/Api/Tools/Models/SendFileModel.cs +++ b/src/Api/Tools/Models/SendFileModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; diff --git a/src/Api/Tools/Models/SendTextModel.cs b/src/Api/Tools/Models/SendTextModel.cs index 274e0d537a..fdc547c522 100644 --- a/src/Api/Tools/Models/SendTextModel.cs +++ b/src/Api/Tools/Models/SendTextModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Tools.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; namespace Bit.Api.Tools.Models; diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 829eedc34d..c7f7e3aff7 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; diff --git a/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs index f41c62f409..82232cb757 100644 --- a/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Exceptions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs index 804200a05f..9655d155e6 100644 --- a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Tools.Entities; diff --git a/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs index ee54ffd6b6..748a4e1d07 100644 --- a/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs +++ b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs @@ -1,4 +1,7 @@ -using Azure.Storage.Blobs; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Azure.Storage.Sas; using Bit.Core.Enums; diff --git a/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs index a6b3fb0faf..6722e4f46b 100644 --- a/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs +++ b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Settings; using Bit.Core.Tools.Entities; diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index f1e8855def..c6dd3b1dc9 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; diff --git a/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs b/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs index 16c20e521e..3aeecde4fd 100644 --- a/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs +++ b/src/Core/Tools/Services/NoopImplementations/NoopSendFileStorageService.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; using Bit.Core.Tools.Entities; namespace Bit.Core.Tools.Services; diff --git a/src/Infrastructure.EntityFramework/Tools/Models/Send.cs b/src/Infrastructure.EntityFramework/Tools/Models/Send.cs index 1d9c8ae181..0eb4ab39a2 100644 --- a/src/Infrastructure.EntityFramework/Tools/Models/Send.cs +++ b/src/Infrastructure.EntityFramework/Tools/Models/Send.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.Models; From da66400248f3e7812467cb575ee3583f9c149269 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:32:49 -0400 Subject: [PATCH 035/326] Add `#nullable disable` to AC code (#6052) --- .../Providers/RemoveOrganizationFromProviderCommand.cs | 5 ++++- .../AdminConsole/Services/ProviderService.cs | 5 ++++- bitwarden_license/src/Scim/Context/ScimContext.cs | 5 ++++- .../src/Scim/Controllers/v2/GroupsController.cs | 5 ++++- .../src/Scim/Controllers/v2/UsersController.cs | 5 ++++- bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs | 5 ++++- bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs | 5 ++++- bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/BaseScimModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/BaseScimUserModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimListResponseModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimPatchModel.cs | 5 ++++- bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs | 5 ++++- bitwarden_license/src/Scim/Users/GetUsersListQuery.cs | 5 ++++- .../src/Scim/Users/Interfaces/IPostUserCommand.cs | 5 ++++- .../AdminConsole/Controllers/OrganizationsController.cs | 5 ++++- src/Admin/AdminConsole/Controllers/ProvidersController.cs | 5 ++++- .../AdminConsole/Models/CreateBusinessUnitProviderModel.cs | 5 ++++- src/Admin/AdminConsole/Models/CreateMspProviderModel.cs | 5 ++++- .../AdminConsole/Models/CreateResellerProviderModel.cs | 5 ++++- src/Admin/AdminConsole/Models/OrganizationEditModel.cs | 5 ++++- .../AdminConsole/Models/OrganizationInitiateDeleteModel.cs | 5 ++++- .../OrganizationUnassignedToProviderSearchViewModel.cs | 5 ++++- src/Admin/AdminConsole/Models/OrganizationViewModel.cs | 5 ++++- src/Admin/AdminConsole/Models/OrganizationsModel.cs | 5 ++++- src/Admin/AdminConsole/Models/ProviderEditModel.cs | 5 ++++- src/Admin/AdminConsole/Models/ProviderViewModel.cs | 5 ++++- src/Admin/AdminConsole/Models/ProvidersModel.cs | 5 ++++- .../AdminConsole/Views/Organizations/Connections.cshtml | 2 +- src/Admin/AdminConsole/Views/Providers/Admins.cshtml | 2 +- .../AdminConsole/Views/Providers/CreateOrganization.cshtml | 2 +- src/Api/AdminConsole/Controllers/EventsController.cs | 5 ++++- src/Api/AdminConsole/Controllers/GroupsController.cs | 5 ++++- .../Controllers/OrganizationConnectionsController.cs | 5 ++++- .../AdminConsole/Controllers/OrganizationUsersController.cs | 5 ++++- src/Api/AdminConsole/Controllers/OrganizationsController.cs | 5 ++++- src/Api/AdminConsole/Controllers/PoliciesController.cs | 5 ++++- .../AdminConsole/Controllers/ProviderClientsController.cs | 5 ++++- .../Controllers/ProviderOrganizationsController.cs | 5 ++++- src/Api/AdminConsole/Controllers/ProviderUsersController.cs | 5 ++++- src/Api/AdminConsole/Controllers/ProvidersController.cs | 5 ++++- .../AdminConsole/Controllers/SlackIntegrationController.cs | 5 ++++- .../Models/Request/AdminAuthRequestUpdateRequestModel.cs | 5 ++++- .../Models/Request/BulkDenyAdminAuthRequestRequestModel.cs | 5 ++++- src/Api/AdminConsole/Models/Request/GroupRequestModel.cs | 5 ++++- .../OrganizationAuthRequestUpdateManyRequestModel.cs | 5 ++++- .../Models/Request/OrganizationDomainRequestModel.cs | 5 ++++- .../Organizations/OrganizationConnectionRequestModel.cs | 5 ++++- .../Request/Organizations/OrganizationCreateRequestModel.cs | 5 ++++- .../OrganizationDomainSsoDetailsRequestModel.cs | 5 ++++- .../Request/Organizations/OrganizationKeysRequestModel.cs | 5 ++++- .../Organizations/OrganizationNoPaymentCreateRequest.cs | 5 ++++- .../Request/Organizations/OrganizationUpdateRequestModel.cs | 5 ++++- .../Organizations/OrganizationUpgradeRequestModel.cs | 5 ++++- .../Request/Organizations/OrganizationUserRequestModels.cs | 5 ++++- .../OrganizationVerifyDeleteRecoverRequestModel.cs | 5 ++++- src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs | 5 ++++- .../Providers/ProviderOrganizationAddRequestModel.cs | 5 ++++- .../Providers/ProviderOrganizationCreateRequestModel.cs | 5 ++++- .../Models/Request/Providers/ProviderSetupRequestModel.cs | 5 ++++- .../Models/Request/Providers/ProviderUpdateRequestModel.cs | 5 ++++- .../Models/Request/Providers/ProviderUserRequestModels.cs | 5 ++++- .../Providers/ProviderVerifyDeleteRecoverRequestModel.cs | 5 ++++- src/Api/AdminConsole/Models/Response/GroupResponseModel.cs | 5 ++++- .../Organizations/OrganizationConnectionResponseModel.cs | 5 ++++- .../Response/Organizations/OrganizationKeysResponseModel.cs | 5 ++++- .../Organizations/OrganizationPublicKeyResponseModel.cs | 5 ++++- .../Response/Organizations/OrganizationResponseModel.cs | 5 ++++- .../Response/Organizations/OrganizationUserResponseModel.cs | 5 ++++- .../Models/Response/Organizations/PolicyResponseModel.cs | 5 ++++- .../VerifiedOrganizationDomainSsoDetailsResponseModel.cs | 5 ++++- .../Response/PendingOrganizationAuthRequestResponseModel.cs | 5 ++++- .../Models/Response/ProfileOrganizationResponseModel.cs | 5 ++++- .../Response/Providers/ProviderOrganizationResponseModel.cs | 5 ++++- .../Models/Response/Providers/ProviderResponseModel.cs | 5 ++++- src/Api/AdminConsole/Public/Controllers/EventsController.cs | 5 ++++- src/Api/AdminConsole/Public/Controllers/GroupsController.cs | 5 ++++- .../AdminConsole/Public/Controllers/MembersController.cs | 5 ++++- .../Public/Controllers/OrganizationController.cs | 5 ++++- .../AdminConsole/Public/Controllers/PoliciesController.cs | 5 ++++- src/Api/AdminConsole/Public/Models/GroupBaseModel.cs | 5 ++++- src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs | 5 ++++- .../Request/AssociationWithPermissionsRequestModel.cs | 5 ++++- .../Public/Models/Request/EventFilterRequestModel.cs | 5 ++++- .../Public/Models/Request/GroupCreateUpdateRequestModel.cs | 5 ++++- .../Public/Models/Request/MemberCreateRequestModel.cs | 5 ++++- .../Public/Models/Request/MemberUpdateRequestModel.cs | 5 ++++- .../Public/Models/Request/OrganizationImportRequestModel.cs | 5 ++++- .../Public/Models/Request/UpdateGroupIdsRequestModel.cs | 5 ++++- .../Public/Models/Request/UpdateMemberIdsRequestModel.cs | 5 ++++- .../Public/Models/Response/GroupResponseModel.cs | 5 ++++- .../Public/Models/Response/MemberResponseModel.cs | 5 ++++- .../Public/Models/Response/PolicyResponseModel.cs | 5 ++++- src/Core/AdminConsole/Models/Data/IEvent.cs | 5 ++++- src/Core/AdminConsole/Services/IEventMessageHandler.cs | 5 ++++- src/Core/AdminConsole/Services/IEventService.cs | 5 ++++- src/Core/AdminConsole/Services/IOrganizationService.cs | 5 ++++- src/Events/Controllers/CollectController.cs | 5 ++++- src/EventsProcessor/AzureQueueHostedService.cs | 5 ++++- .../AdminConsole/Models/Organization.cs | 5 ++++- .../AdminConsole/Models/OrganizationIntegration.cs | 5 ++++- .../Models/OrganizationIntegrationConfiguration.cs | 5 ++++- .../AdminConsole/Models/Policy.cs | 5 ++++- .../AdminConsole/Models/Provider/ProviderOrganization.cs | 5 ++++- .../AdminConsole/Models/Provider/ProviderUser.cs | 5 ++++- .../AdminConsole/Repositories/GroupRepository.cs | 5 ++++- .../AdminConsole/Repositories/OrganizationRepository.cs | 5 ++++- .../AdminConsole/Repositories/OrganizationUserRepository.cs | 5 ++++- .../AdminConsole/Repositories/PolicyRepository.cs | 5 ++++- .../Repositories/ProviderOrganizationRepository.cs | 5 ++++- .../AdminConsole/Repositories/ProviderRepository.cs | 5 ++++- .../AdminConsole/Repositories/ProviderUserRepository.cs | 5 ++++- .../AzureServiceBusIntegrationListenerServiceTests.cs | 6 +++--- 116 files changed, 454 insertions(+), 118 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 4af0e12e64..824691d8d2 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 3c75be756a..5a0ae68631 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; diff --git a/bitwarden_license/src/Scim/Context/ScimContext.cs b/bitwarden_license/src/Scim/Context/ScimContext.cs index efcc8dbde3..bb0286b919 100644 --- a/bitwarden_license/src/Scim/Context/ScimContext.cs +++ b/bitwarden_license/src/Scim/Context/ScimContext.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Enums; diff --git a/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs b/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs index 6da4001753..e3c290c85f 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs index 77bc62e952..4d292281dd 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs b/bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs index 7055736a4c..cc6546700b 100644 --- a/bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs +++ b/bitwarden_license/src/Scim/Groups/GetGroupsListQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Repositories; using Bit.Scim.Groups.Interfaces; diff --git a/bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs b/bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs index ab082fc2a6..c83b2c0493 100644 --- a/bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs +++ b/bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// 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.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.Repositories; diff --git a/bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs b/bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs index 150885fb50..6004c7572a 100644 --- a/bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs +++ b/bitwarden_license/src/Scim/Models/BaseScimGroupModel.cs @@ -1,4 +1,7 @@ -using Bit.Scim.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Scim.Utilities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/BaseScimModel.cs b/bitwarden_license/src/Scim/Models/BaseScimModel.cs index 8f3adfbe4a..f4e0d9efdb 100644 --- a/bitwarden_license/src/Scim/Models/BaseScimModel.cs +++ b/bitwarden_license/src/Scim/Models/BaseScimModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Scim.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Scim.Models; public abstract class BaseScimModel { diff --git a/bitwarden_license/src/Scim/Models/BaseScimUserModel.cs b/bitwarden_license/src/Scim/Models/BaseScimUserModel.cs index d3c69d574d..eb8ffe88a6 100644 --- a/bitwarden_license/src/Scim/Models/BaseScimUserModel.cs +++ b/bitwarden_license/src/Scim/Models/BaseScimUserModel.cs @@ -1,4 +1,7 @@ -using Bit.Scim.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Scim.Utilities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs b/bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs index d1dce35ef0..064acc476b 100644 --- a/bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimErrorResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Scim.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Scim.Utilities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs b/bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs index 11bd40c587..3a9c795f58 100644 --- a/bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimGroupRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Utilities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs b/bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs index 697a3d59da..a3d7c2054a 100644 --- a/bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimGroupResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimListResponseModel.cs b/bitwarden_license/src/Scim/Models/ScimListResponseModel.cs index 77ab52356c..9f5cc61f97 100644 --- a/bitwarden_license/src/Scim/Models/ScimListResponseModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimListResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Scim.Utilities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Scim.Utilities; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimPatchModel.cs b/bitwarden_license/src/Scim/Models/ScimPatchModel.cs index 6707ced85f..5392a18e3c 100644 --- a/bitwarden_license/src/Scim/Models/ScimPatchModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimPatchModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; namespace Bit.Scim.Models; diff --git a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs index 295db790e3..0baf6469ff 100644 --- a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 9bcbcbdafc..a734635ebf 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Scim.Users.Interfaces; diff --git a/bitwarden_license/src/Scim/Users/Interfaces/IPostUserCommand.cs b/bitwarden_license/src/Scim/Users/Interfaces/IPostUserCommand.cs index 05dd05510c..401754ad10 100644 --- a/bitwarden_license/src/Scim/Users/Interfaces/IPostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/Interfaces/IPostUserCommand.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Scim.Models; namespace Bit.Scim.Users.Interfaces; diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index ecdd372df4..4bbb5db3f0 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; using Bit.Admin.Services; diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 7f11b65d9e..4fc9556a66 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Net; using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; diff --git a/src/Admin/AdminConsole/Models/CreateBusinessUnitProviderModel.cs b/src/Admin/AdminConsole/Models/CreateBusinessUnitProviderModel.cs index b57d90e33b..fd83ba8e5d 100644 --- a/src/Admin/AdminConsole/Models/CreateBusinessUnitProviderModel.cs +++ b/src/Admin/AdminConsole/Models/CreateBusinessUnitProviderModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; diff --git a/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs index 4ada2d4a5f..4832910d4c 100644 --- a/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs +++ b/src/Admin/AdminConsole/Models/CreateMspProviderModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.SharedWeb.Utilities; diff --git a/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs b/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs index 958faf3f85..0bb3ea47bb 100644 --- a/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs +++ b/src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.SharedWeb.Utilities; diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index c79124688e..b64af3135f 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Net; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; diff --git a/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs b/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs index 5e9055be55..f3d9ae1dd8 100644 --- a/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Admin.AdminConsole.Models; diff --git a/src/Admin/AdminConsole/Models/OrganizationUnassignedToProviderSearchViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationUnassignedToProviderSearchViewModel.cs index cbf15a4776..26c27f01b5 100644 --- a/src/Admin/AdminConsole/Models/OrganizationUnassignedToProviderSearchViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationUnassignedToProviderSearchViewModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Admin.Models; namespace Bit.Admin.AdminConsole.Models; diff --git a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs index 412b17b3d7..2c126ecd8e 100644 --- a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Entities; using Bit.Core.Enums; diff --git a/src/Admin/AdminConsole/Models/OrganizationsModel.cs b/src/Admin/AdminConsole/Models/OrganizationsModel.cs index a98985ef01..6bfec24486 100644 --- a/src/Admin/AdminConsole/Models/OrganizationsModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationsModel.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Admin.Models; using Bit.Core.AdminConsole.Entities; namespace Bit.Admin.AdminConsole.Models; diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index de9e25fa6f..51fe4bbe64 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index e1277f8e87..f6e16d270d 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Billing.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Admin.Billing.Models; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; diff --git a/src/Admin/AdminConsole/Models/ProvidersModel.cs b/src/Admin/AdminConsole/Models/ProvidersModel.cs index 6de815facf..ea7b0aa4f0 100644 --- a/src/Admin/AdminConsole/Models/ProvidersModel.cs +++ b/src/Admin/AdminConsole/Models/ProvidersModel.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Admin.Models; using Bit.Core.AdminConsole.Entities.Provider; namespace Bit.Admin.AdminConsole.Models; diff --git a/src/Admin/AdminConsole/Views/Organizations/Connections.cshtml b/src/Admin/AdminConsole/Views/Organizations/Connections.cshtml index 6efdb34b20..7d2a409715 100644 --- a/src/Admin/AdminConsole/Views/Organizations/Connections.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/Connections.cshtml @@ -52,7 +52,7 @@ @if(connection.Enabled) { - @if(@TempData["ConnectionActivated"] != null && @TempData["ConnectionActivated"].ToString() == @Model.Organization.Id.ToString()) + @if(@TempData["ConnectionActivated"] != null && @TempData["ConnectionActivated"]!.ToString() == @Model.Organization.Id.ToString()) { @if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync)) { diff --git a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml index 29eddc8964..fb258bec46 100644 --- a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml @@ -53,7 +53,7 @@ && @Model.Provider.Status.Equals(ProviderStatusType.Pending) && canResendEmailInvite) { - @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @user.UserId.Value.ToString()) + @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"]!.ToString() == @user.UserId!.Value.ToString()) { } diff --git a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml index eb790f20ba..148c2e0c2d 100644 --- a/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/CreateOrganization.cshtml @@ -8,7 +8,7 @@ } diff --git a/src/Api/AdminConsole/Controllers/EventsController.cs b/src/Api/AdminConsole/Controllers/EventsController.cs index 921ee84400..d555c7321d 100644 --- a/src/Api/AdminConsole/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Controllers/EventsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index 946d7399c2..f8e97881cb 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.AuthorizationHandlers.Collections; diff --git a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs index 8e54bfca9c..5da090314b 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 7765eb2665..81c31355e3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Authorization.Requirements; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 0d498beab1..18045178db 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response; using Bit.Api.AdminConsole.Models.Response.Organizations; diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 86a1609ee6..a80546e2f5 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index f226ba316e..caf2651e16 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.Billing.Controllers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; diff --git a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs index 12166c836e..f68b036be4 100644 --- a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Providers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Providers.Interfaces; diff --git a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs index 73639bb1a4..b89f553325 100644 --- a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Providers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Models.Business.Provider; diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs index b6933da0c9..d8bda2ca18 100644 --- a/src/Api/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs @@ -1,4 +1,7 @@ -using Bit.Api.AdminConsole.Models.Request.Providers; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.AdminConsole.Models.Request.Providers; using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs index 3d749d25d7..6e3751c6f6 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core; using Bit.Core.AdminConsole.Entities; diff --git a/src/Api/AdminConsole/Models/Request/AdminAuthRequestUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/AdminAuthRequestUpdateRequestModel.cs index abcc6fdb74..13c840ced4 100644 --- a/src/Api/AdminConsole/Models/Request/AdminAuthRequestUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/AdminAuthRequestUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Api.AdminConsole.Models.Request; diff --git a/src/Api/AdminConsole/Models/Request/BulkDenyAdminAuthRequestRequestModel.cs b/src/Api/AdminConsole/Models/Request/BulkDenyAdminAuthRequestRequestModel.cs index 24386341a3..86e058b847 100644 --- a/src/Api/AdminConsole/Models/Request/BulkDenyAdminAuthRequestRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/BulkDenyAdminAuthRequestRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.AdminConsole.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.AdminConsole.Models.Request; public class BulkDenyAdminAuthRequestRequestModel { diff --git a/src/Api/AdminConsole/Models/Request/GroupRequestModel.cs b/src/Api/AdminConsole/Models/Request/GroupRequestModel.cs index a6cfb6733b..007b3d3949 100644 --- a/src/Api/AdminConsole/Models/Request/GroupRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/GroupRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Models.Request; using Bit.Core.AdminConsole.Entities; diff --git a/src/Api/AdminConsole/Models/Request/OrganizationAuthRequestUpdateManyRequestModel.cs b/src/Api/AdminConsole/Models/Request/OrganizationAuthRequestUpdateManyRequestModel.cs index 34a45369b2..bd5e647c84 100644 --- a/src/Api/AdminConsole/Models/Request/OrganizationAuthRequestUpdateManyRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/OrganizationAuthRequestUpdateManyRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.OrganizationAuth.Models; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.OrganizationAuth.Models; using Bit.Core.Utilities; namespace Bit.Api.AdminConsole.Models.Request; diff --git a/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs b/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs index 8bf1ebe39a..46b253da31 100644 --- a/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/OrganizationDomainRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Models.Request; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationConnectionRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationConnectionRequestModel.cs index d7508b78ef..1dbd624cbe 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationConnectionRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationConnectionRequestModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationConnections; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index e18122fd2b..10f938adfe 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.Billing.Enums; using Bit.Core.Entities; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationDomainSsoDetailsRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationDomainSsoDetailsRequestModel.cs index c5129c6ec7..b4eafc095f 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationDomainSsoDetailsRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationDomainSsoDetailsRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Models.Request.Organizations; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs index 4afa5b54ea..22b225a689 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Business; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs index 3255c8b413..0c62b23518 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.Billing.Enums; using Bit.Core.Entities; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs index decc04a0db..5a3192c121 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Data; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index 2a73f094ed..a5dec192b9 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs index e6d4f85d3b..b4d3326013 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Models.Request; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs index 36dba6ed98..0963887994 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Models.Request.Organizations; diff --git a/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs b/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs index a243f46b2e..0e31deacd1 100644 --- a/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/PolicyRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationAddRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationAddRequestModel.cs index 207d84b787..9a33431443 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationAddRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationAddRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Models.Request.Providers; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationCreateRequestModel.cs index bf75c611e2..25417d04c5 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderOrganizationCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Core.Utilities; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs index 697077c9b6..1f50c384a3 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Api.Billing.Models.Requests; using Bit.Api.Models.Request; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderUpdateRequestModel.cs index e41cb13f4e..8a7ab7643b 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Settings; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderUserRequestModels.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderUserRequestModels.cs index dd22530916..12b1e0d064 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderUserRequestModels.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderUserRequestModels.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Utilities; diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderVerifyDeleteRecoverRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderVerifyDeleteRecoverRequestModel.cs index edb58c21b1..a3a0f4fba6 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderVerifyDeleteRecoverRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderVerifyDeleteRecoverRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Models.Request.Providers; diff --git a/src/Api/AdminConsole/Models/Response/GroupResponseModel.cs b/src/Api/AdminConsole/Models/Response/GroupResponseModel.cs index f956f27ebc..741473a5c4 100644 --- a/src/Api/AdminConsole/Models/Response/GroupResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/GroupResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationConnectionResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationConnectionResponseModel.cs index fa6bdc1f3d..f365080b73 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationConnectionResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationConnectionResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationKeysResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationKeysResponseModel.cs index 15dbb18102..1b82d20fb8 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationKeysResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationKeysResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Api; namespace Bit.Api.AdminConsole.Models.Response.Organizations; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationPublicKeyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationPublicKeyResponseModel.cs index defae9ba4d..b938fd9893 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationPublicKeyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationPublicKeyResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Api; namespace Bit.Api.AdminConsole.Models.Response.Organizations; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 95754598b9..62d7343509 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 057841c7d2..7c31c2ae81 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Api.Models.Response; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs index 86e62a4193..9feafce70c 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// 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.Enums; using Bit.Core.Models.Api; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs index 3488eab2c8..178060d9b1 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/VerifiedOrganizationDomainSsoDetailsResponseModel.cs @@ -1,4 +1,7 @@ -using Bit.Api.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Api.Models.Response; namespace Bit.Api.AdminConsole.Models.Response.Organizations; diff --git a/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs b/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs index 3e242cba7b..8952270adf 100644 --- a/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/PendingOrganizationAuthRequestResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Auth.Models.Data; using Bit.Core.Models.Api; diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index cb0ab62fd1..e421c3247e 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; diff --git a/src/Api/AdminConsole/Models/Response/Providers/ProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Providers/ProviderOrganizationResponseModel.cs index 963fbaa209..c0b492df95 100644 --- a/src/Api/AdminConsole/Models/Response/Providers/ProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Providers/ProviderOrganizationResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Models.Api; diff --git a/src/Api/AdminConsole/Models/Response/Providers/ProviderResponseModel.cs b/src/Api/AdminConsole/Models/Response/Providers/ProviderResponseModel.cs index 291fb24829..5031c4963d 100644 --- a/src/Api/AdminConsole/Models/Response/Providers/ProviderResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Providers/ProviderResponseModel.cs @@ -1,4 +1,7 @@ -using System.Text.Json.Serialization; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Models.Api; diff --git a/src/Api/AdminConsole/Public/Controllers/EventsController.cs b/src/Api/AdminConsole/Public/Controllers/EventsController.cs index 992b7453aa..3dd55d51e2 100644 --- a/src/Api/AdminConsole/Public/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Public/Controllers/EventsController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.Models.Public.Request; using Bit.Api.Models.Public.Response; using Bit.Core.Context; diff --git a/src/Api/AdminConsole/Public/Controllers/GroupsController.cs b/src/Api/AdminConsole/Public/Controllers/GroupsController.cs index 9ce22536b1..9644d2d799 100644 --- a/src/Api/AdminConsole/Public/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Public/Controllers/GroupsController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 90c78c9eb7..6f41016dcd 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; diff --git a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs index c1715f471c..a1af1c3fb8 100644 --- a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs +++ b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.Models.Public.Response; using Bit.Core.Context; diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index d261a3c555..1caf9cb068 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -1,4 +1,7 @@ -using System.Net; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; diff --git a/src/Api/AdminConsole/Public/Models/GroupBaseModel.cs b/src/Api/AdminConsole/Public/Models/GroupBaseModel.cs index fd42cccffd..d21e9d757f 100644 --- a/src/Api/AdminConsole/Public/Models/GroupBaseModel.cs +++ b/src/Api/AdminConsole/Public/Models/GroupBaseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Public.Models; diff --git a/src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs b/src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs index f474d87ec9..ba455d92e1 100644 --- a/src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs +++ b/src/Api/AdminConsole/Public/Models/PolicyBaseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; namespace Bit.Api.AdminConsole.Public.Models; diff --git a/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs index 202bd5f705..7a76526ede 100644 --- a/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/AssociationWithPermissionsRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data; namespace Bit.Api.AdminConsole.Public.Models.Request; diff --git a/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs index 852076eebc..2d96425d55 100644 --- a/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/EventFilterRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.Exceptions; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Exceptions; namespace Bit.Api.Models.Public.Request; diff --git a/src/Api/AdminConsole/Public/Models/Request/GroupCreateUpdateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/GroupCreateUpdateRequestModel.cs index 671503c649..3c531b4208 100644 --- a/src/Api/AdminConsole/Public/Models/Request/GroupCreateUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/GroupCreateUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.AdminConsole.Entities; namespace Bit.Api.AdminConsole.Public.Models.Request; diff --git a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs index f6b2c4d4af..6813610325 100644 --- a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Api/AdminConsole/Public/Models/Request/MemberUpdateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/MemberUpdateRequestModel.cs index ac281e3c44..674fa1290f 100644 --- a/src/Api/AdminConsole/Public/Models/Request/MemberUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/MemberUpdateRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Api/AdminConsole/Public/Models/Request/OrganizationImportRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/OrganizationImportRequestModel.cs index 2adda81e49..6122d5dfd0 100644 --- a/src/Api/AdminConsole/Public/Models/Request/OrganizationImportRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/OrganizationImportRequestModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; diff --git a/src/Api/AdminConsole/Public/Models/Request/UpdateGroupIdsRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/UpdateGroupIdsRequestModel.cs index c55be36fff..f3714025ac 100644 --- a/src/Api/AdminConsole/Public/Models/Request/UpdateGroupIdsRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/UpdateGroupIdsRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.AdminConsole.Public.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.AdminConsole.Public.Models.Request; public class UpdateGroupIdsRequestModel { diff --git a/src/Api/AdminConsole/Public/Models/Request/UpdateMemberIdsRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/UpdateMemberIdsRequestModel.cs index 4124719929..bf0ea342ac 100644 --- a/src/Api/AdminConsole/Public/Models/Request/UpdateMemberIdsRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/UpdateMemberIdsRequestModel.cs @@ -1,4 +1,7 @@ -namespace Bit.Api.AdminConsole.Public.Models.Request; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Api.AdminConsole.Public.Models.Request; public class UpdateMemberIdsRequestModel { diff --git a/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs index c275d1658b..c12616b4cc 100644 --- a/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/GroupResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Models.Data; diff --git a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs index 933cda9dca..70da584621 100644 --- a/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/MemberResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Bit.Api.Models.Public.Response; diff --git a/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs index 8da7d93cf1..e43f994255 100644 --- a/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/PolicyResponseModel.cs @@ -1,4 +1,7 @@ -using System.ComponentModel.DataAnnotations; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.ComponentModel.DataAnnotations; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; diff --git a/src/Core/AdminConsole/Models/Data/IEvent.cs b/src/Core/AdminConsole/Models/Data/IEvent.cs index 6a177e39ca..7cdcf06eaf 100644 --- a/src/Core/AdminConsole/Models/Data/IEvent.cs +++ b/src/Core/AdminConsole/Models/Data/IEvent.cs @@ -1,4 +1,7 @@ -using Bit.Core.Enums; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Enums; namespace Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/AdminConsole/Services/IEventMessageHandler.cs index 83c5e33ecb..fcffb56c65 100644 --- a/src/Core/AdminConsole/Services/IEventMessageHandler.cs +++ b/src/Core/AdminConsole/Services/IEventMessageHandler.cs @@ -1,4 +1,7 @@ -using Bit.Core.Models.Data; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Models.Data; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IEventService.cs b/src/Core/AdminConsole/Services/IEventService.cs index 14ef4ba4d4..ba6d4da8f5 100644 --- a/src/Core/AdminConsole/Services/IEventService.cs +++ b/src/Core/AdminConsole/Services/IEventService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Interfaces; using Bit.Core.Entities; diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index feae561a19..297184430c 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -1,4 +1,7 @@ -using Bit.Core.AdminConsole.Entities; +// 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.Models.Business; using Bit.Core.Auth.Enums; using Bit.Core.Entities; diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index 5eb48a2688..d7fbbbc595 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -1,4 +1,7 @@ -using Bit.Core.Context; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/EventsProcessor/AzureQueueHostedService.cs b/src/EventsProcessor/AzureQueueHostedService.cs index b1b309b50f..1f72fbb9c8 100644 --- a/src/EventsProcessor/AzureQueueHostedService.cs +++ b/src/EventsProcessor/AzureQueueHostedService.cs @@ -1,4 +1,7 @@ -using System.Text.Json; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using System.Text.Json; using Azure.Storage.Queues; using Bit.Core; using Bit.Core.Models.Data; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs index d7f83d829d..ac50b64f3a 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/Organization.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Infrastructure.EntityFramework.Auth.Models; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs index db81b81166..5e5f7d4802 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs index 465a49dc02..52b8783fcf 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Policy.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/Policy.cs index 0685789e2b..e58e2874e5 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/Policy.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/Policy.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderOrganization.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderOrganization.cs index e02dfbefec..d00ecf7277 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderOrganization.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderOrganization.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderUser.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderUser.cs index 1b6de00960..af2d79a4aa 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderUser.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/Provider/ProviderUser.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Infrastructure.EntityFramework.Models; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs index 305a715d4c..3b6ea749fa 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/GroupRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Models.Data; using Bit.Infrastructure.EntityFramework.Models; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index c378fe5e7e..53216b9d78 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using AutoMapper.QueryableExtensions; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Constants; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 26a72bb991..f311baec90 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index 0564681341..f9287a20a9 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderOrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderOrganizationRepository.cs index 77f5f8edc1..f9ef44fb9a 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderOrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderOrganizationRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderRepository.cs index 3c2ac73b83..2a9b3b8abe 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs index ad4422da63..5474e3e217 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/ProviderUserRepository.cs @@ -1,4 +1,7 @@ -using AutoMapper; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +using AutoMapper; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index 32a305266d..740baec37e 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -63,7 +63,7 @@ public class AzureServiceBusIntegrationListenerServiceTests Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); } [Theory, BitAutoData] @@ -81,7 +81,7 @@ public class AzureServiceBusIntegrationListenerServiceTests Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); } [Theory, BitAutoData] @@ -114,6 +114,6 @@ public class AzureServiceBusIntegrationListenerServiceTests Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); } } From 2c58896c7ebab317d9a043f4b60878d1c020abac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:13:42 +0200 Subject: [PATCH 036/326] [deps] Tools: Update aws-sdk-net monorepo (#6071) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index e1c476bebc..7c2e6ef3e2 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 4cd930caff7d7fef4b9cdafdd389f9d22b5caab7 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 9 Jul 2025 10:41:17 -0400 Subject: [PATCH 037/326] Turn NRT on by default in all new projects/files (#6069) --- Directory.Build.props | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e68ae70fb1..c080e2eff4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,8 +11,7 @@ true annotations - - + enable true @@ -69,4 +68,4 @@ - \ No newline at end of file + From 12b2eeaa66353bb3b90e2d36f7f8380899fa02ec Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 9 Jul 2025 08:26:49 -0700 Subject: [PATCH 038/326] [PM-22136] Add SDK Cipher Encryption feature flag (#6070) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7a3d462905..2a3b619de6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -209,6 +209,7 @@ public static class FeatureFlagKeys public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy"; 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 static List GetAllKeys() { From 5772c467de32a65ae438035ae50c8768358505c2 Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Wed, 9 Jul 2025 15:02:11 -0400 Subject: [PATCH 039/326] [BRE-831] migrate secrets AKV (#5962) --- .../_move_finalization_db_scripts.yml | 28 +++++-- .github/workflows/build.yml | 80 +++++++++++++------ .github/workflows/build_target.yml | 7 ++ .github/workflows/cleanup-after-pr.yml | 13 ++- .github/workflows/cleanup-rc-branch.yml | 14 +++- .github/workflows/code-references.yml | 39 ++++++--- .github/workflows/ephemeral-environment.yml | 4 + .github/workflows/publish.yml | 19 ++++- .github/workflows/repository-management.yml | 48 ++++++++++- .github/workflows/scan.yml | 50 ++++++++++-- 10 files changed, 241 insertions(+), 61 deletions(-) diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index d897875394..33d828fef7 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -12,14 +12,19 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write outputs: migration_filename_prefix: ${{ steps.prefix.outputs.prefix }} copy_finalization_scripts: ${{ steps.check-finalization-scripts-existence.outputs.copy_finalization_scripts }} steps: - name: Log in to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve secrets id: retrieve-secrets @@ -28,6 +33,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Check out branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: @@ -50,6 +58,11 @@ jobs: name: Move finalization database scripts runs-on: ubuntu-22.04 needs: setup + permissions: + contents: write + pull-requests: write + id-token: write + actions: read if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout @@ -92,10 +105,12 @@ jobs: done echo "moved_files=$moved_files" >> $GITHUB_OUTPUT - - name: Log in to Azure - production subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve secrets id: retrieve-secrets @@ -106,6 +121,9 @@ jobs: github-gpg-private-key-passphrase, devops-alerts-slack-webhook-url" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Import GPG keys uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 with: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7d4a83fed..7c170f1188 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: types: [opened, synchronize] workflow_call: inputs: {} - + permissions: contents: read @@ -95,10 +95,8 @@ jobs: steps: - name: Check secrets id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} + has_secrets=${{ secrets.AZURE_CLIENT_ID != '' }} echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT - name: Check out repo @@ -174,19 +172,16 @@ jobs: uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 ########## ACRs ########## - - name: Log in to Azure - production subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Log in to ACR - production subscription run: az acr login -n bitwardenprod - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - name: Retrieve GitHub PAT secrets id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@main @@ -287,10 +282,16 @@ jobs: sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + upload: name: Upload runs-on: ubuntu-24.04 needs: build-artifacts + permissions: + id-token: write + actions: read steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -300,10 +301,12 @@ jobs: - name: Set up .NET uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - name: Log in to Azure - production subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Log in to ACR - production subscription run: az acr login -n $_AZ_REGISTRY --only-show-errors @@ -350,6 +353,9 @@ jobs: 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 + - name: Upload Docker stub US artifact if: | github.event_name != 'pull_request' @@ -496,11 +502,15 @@ jobs: runs-on: ubuntu-24.04 needs: - build-artifacts + permissions: + id-token: write steps: - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve GitHub PAT secrets id: retrieve-secret-pat @@ -509,6 +519,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Trigger self-host build uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: @@ -530,11 +543,15 @@ jobs: runs-on: ubuntu-22.04 needs: - build-artifacts + permissions: + id-token: write steps: - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve GitHub PAT secrets id: retrieve-secret-pat @@ -543,6 +560,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Trigger k8s deploy uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: @@ -572,7 +592,9 @@ jobs: project: server pull_request_number: ${{ github.event.number || 0 }} secrets: inherit - permissions: read-all + permissions: + contents: read + id-token: write check-failures: name: Check for failures @@ -585,6 +607,8 @@ jobs: - build-mssqlmigratorutility - self-host-build - trigger-k8s-deploy + permissions: + id-token: write steps: - name: Check if any job failed if: | @@ -593,11 +617,12 @@ jobs: && contains(needs.*.result, 'failure') run: exit 1 - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - if: failure() + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve secrets id: retrieve-secrets @@ -607,6 +632,9 @@ jobs: keyvault: "bitwarden-ci" secrets: "devops-alerts-slack-webhook-url" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Notify Slack on failure uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() diff --git a/.github/workflows/build_target.yml b/.github/workflows/build_target.yml index d825721a7d..4b02ef2f4b 100644 --- a/.github/workflows/build_target.yml +++ b/.github/workflows/build_target.yml @@ -14,6 +14,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read run-workflow: name: Run Build on PR Target @@ -21,3 +23,8 @@ jobs: if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} uses: ./.github/workflows/build.yml secrets: inherit + + permissions: + contents: read + id-token: write + security-events: write diff --git a/.github/workflows/cleanup-after-pr.yml b/.github/workflows/cleanup-after-pr.yml index c36dc4a034..e39bf8ea3a 100644 --- a/.github/workflows/cleanup-after-pr.yml +++ b/.github/workflows/cleanup-after-pr.yml @@ -11,11 +11,15 @@ jobs: build-docker: name: Remove branch-specific Docker images runs-on: ubuntu-22.04 + permissions: + id-token: write steps: - - name: Log in to Azure - production subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Log in to Azure ACR run: az acr login -n $_AZ_REGISTRY --only-show-errors @@ -62,3 +66,6 @@ jobs: - name: Log out of Docker run: docker logout + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index 1ea2eab08a..5c74284423 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -9,11 +9,16 @@ jobs: delete-rc: name: Delete RC Branch runs-on: ubuntu-22.04 + permissions: + contents: write + id-token: write steps: - - name: Login to Azure - CI Subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Retrieve bot secrets id: retrieve-bot-secrets @@ -22,6 +27,9 @@ jobs: keyvault: bitwarden-ci secrets: "github-pat-bitwarden-devops-bot-repo-scope" + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Checkout main uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 359e64eb57..75e0c43306 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -1,25 +1,24 @@ name: Collect code references -on: +on: push: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - check-ld-secret: - name: Check for LD secret + check-secret-access: + name: Check for secret access runs-on: ubuntu-22.04 outputs: - available: ${{ steps.check-ld-secret.outputs.available }} - permissions: - contents: read + available: ${{ steps.check-secret-access.outputs.available }} + permissions: {} steps: - name: Check - id: check-ld-secret + id: check-secret-access run: | - if [ "${{ secrets.LD_ACCESS_TOKEN }}" != '' ]; then + if [ "${{ secrets.AZURE_CLIENT_ID }}" != '' ]; then echo "available=true" >> $GITHUB_OUTPUT; else echo "available=false" >> $GITHUB_OUTPUT; @@ -28,21 +27,39 @@ jobs: refs: name: Code reference collection runs-on: ubuntu-22.04 - needs: check-ld-secret - if: ${{ needs.check-ld-secret.outputs.available == 'true' }} + needs: check-secret-access + if: ${{ needs.check-secret-access.outputs.available == 'true' }} permissions: contents: read pull-requests: write + id-token: write steps: - name: Check out repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-server + secrets: "LD-ACCESS-TOKEN" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Collect id: collect uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0 with: - accessToken: ${{ secrets.LD_ACCESS_TOKEN }} + accessToken: ${{ steps.get-kv-secrets.outputs.LD-ACCESS-TOKEN }} projKey: default allowTags: true diff --git a/.github/workflows/ephemeral-environment.yml b/.github/workflows/ephemeral-environment.yml index 6dd89536b6..d85fcf2fd4 100644 --- a/.github/workflows/ephemeral-environment.yml +++ b/.github/workflows/ephemeral-environment.yml @@ -4,6 +4,10 @@ on: pull_request: types: [labeled] +permissions: + contents: read + id-token: write + jobs: setup-ephemeral-environment: name: Setup Ephemeral Environment diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 55220390c4..444c2289d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,6 +26,9 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + permissions: + contents: read + deployments: write outputs: branch-name: ${{ steps.branch.outputs.branch-name }} deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -63,6 +66,9 @@ jobs: name: Publish Docker images runs-on: ubuntu-22.04 needs: setup + permissions: + contents: read + id-token: write env: _RELEASE_VERSION: ${{ needs.setup.outputs.release-version }} _BRANCH_NAME: ${{ needs.setup.outputs.branch-name }} @@ -109,10 +115,12 @@ jobs: echo "project_name=$PROJECT_NAME" >> $GITHUB_OUTPUT ########## ACR PROD ########## - - name: Log in to Azure - production subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} - name: Log in to Azure ACR run: az acr login -n $_AZ_REGISTRY --only-show-errors @@ -152,12 +160,17 @@ jobs: - name: Log out of Docker run: docker logout + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + update-deployment: name: Update Deployment Status runs-on: ubuntu-22.04 needs: - setup - publish-docker + permissions: + deployments: write if: ${{ always() && inputs.publish_type != 'Dry Run' }} steps: - name: Check if any job failed diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index a59bbcfa6c..2d75cbcb4a 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -54,7 +54,27 @@ jobs: - setup outputs: version: ${{ steps.set-final-version-output.outputs.version }} + permissions: + id-token: write + steps: + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "BW-GHAPP-ID,BW-GHAPP-KEY" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Validate version input format if: ${{ inputs.version_number_override != '' }} uses: bitwarden/gh-actions/version-check@main @@ -65,8 +85,8 @@ jobs: uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -158,13 +178,33 @@ jobs: - setup - bump_version runs-on: ubuntu-24.04 + permissions: + id-token: write + steps: + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "BW-GHAPP-ID,BW-GHAPP-KEY" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Generate GH App token uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 id: app-token with: - app-id: ${{ secrets.BW_GHAPP_ID }} - private-key: ${{ secrets.BW_GHAPP_KEY }} + app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} + private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }} - name: Check out target ref uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index f24a0973fd..eb72867fc4 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -20,6 +20,8 @@ jobs: check-run: name: Check PR run uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + permissions: + contents: read sast: name: SAST scan @@ -29,6 +31,7 @@ jobs: contents: read pull-requests: write security-events: write + id-token: write steps: - name: Check out repo @@ -36,16 +39,33 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Scan with Checkmarx uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: project_name: ${{ github.repository }} - cx_tenant: ${{ secrets.CHECKMARX_TENANT }} + cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }} base_uri: https://ast.checkmarx.net/ - cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }} - cx_client_secret: ${{ secrets.CHECKMARX_SECRET }} + cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }} + cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }} additional_params: | --report-format sarif \ --filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \ @@ -65,6 +85,7 @@ jobs: permissions: contents: read pull-requests: write + id-token: write steps: - name: Set up JDK 17 @@ -85,14 +106,31 @@ jobs: - name: Install SonarCloud scanner run: dotnet tool install dotnet-sonarscanner -g + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: gh-org-bitwarden + secrets: "SONAR-TOKEN" + + - name: Log out from Azure + uses: bitwarden/gh-actions/azure-logout@main + - name: Scan with SonarCloud env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }} run: | dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \ /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ /d:sonar.exclusions=test/,bitwarden_license/test/ \ - /o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ + /o:"${{ github.repository_owner }}" /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" \ /d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }} dotnet build - dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + dotnet-sonarscanner end /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" From f5be1ede2f7a2906e4d4868ec59f0f49fb7cb9f7 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 9 Jul 2025 16:05:25 -0500 Subject: [PATCH 040/326] Adding and setting DefaultUserCollectionEmail in the response model (#6074) --- src/Api/Models/Response/CollectionResponseModel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Api/Models/Response/CollectionResponseModel.cs b/src/Api/Models/Response/CollectionResponseModel.cs index 5eb543e864..d679250f05 100644 --- a/src/Api/Models/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Response/CollectionResponseModel.cs @@ -23,6 +23,7 @@ public class CollectionResponseModel : ResponseModel Name = collection.Name; ExternalId = collection.ExternalId; Type = collection.Type; + DefaultUserCollectionEmail = collection.DefaultUserCollectionEmail; } public Guid Id { get; set; } @@ -30,6 +31,7 @@ public class CollectionResponseModel : ResponseModel public string Name { get; set; } public string ExternalId { get; set; } public CollectionType Type { get; set; } + public string DefaultUserCollectionEmail { get; set; } } /// From 3bfc24523efbebd3096308a24e149283edfd93d1 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 10 Jul 2025 09:17:08 -0400 Subject: [PATCH 041/326] Replace `Thread.Sleep` with `Task.Delay` (#6006) --- src/Core/Jobs/BaseJobsHostedService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Jobs/BaseJobsHostedService.cs b/src/Core/Jobs/BaseJobsHostedService.cs index 2ade53c6bb..3e7bce7e0f 100644 --- a/src/Core/Jobs/BaseJobsHostedService.cs +++ b/src/Core/Jobs/BaseJobsHostedService.cs @@ -109,7 +109,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable _logger.LogWarning($"Exception while trying to schedule job: {job.FullName}, {e}"); var random = new Random(); - Thread.Sleep(random.Next(50, 250)); + await Task.Delay(random.Next(50, 250)); } } } From 7f65a655d4dbfe578b8b5f982de34a6dc757562e Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 10 Jul 2025 08:32:25 -0500 Subject: [PATCH 042/326] [PM-21881] Manage payment details outside of checkout (#6032) * Add feature flag * Further establish billing command pattern and use in PreviewTaxAmountCommand * Add billing address models/commands/queries/tests * Update TypeReadingJsonConverter to account for new union types * Add payment method models/commands/queries/tests * Add credit models/commands/queries/tests * Add command/query registrations * Add new endpoints to support new command model and payment functionality * Run dotnet format * Add InjectUserAttribute for easier AccountBillilngVNextController handling * Add InjectOrganizationAttribute for easier OrganizationBillingVNextController handling * Add InjectProviderAttribute for easier ProviderBillingVNextController handling * Add XML documentation for billing command pipeline * Fix StripeConstants post-nullability * More nullability cleanup * Run dotnet format --- .../Attributes/InjectOrganizationAttribute.cs | 61 +++ .../Attributes/InjectProviderAttribute.cs | 80 ++++ .../Billing/Attributes/InjectUserAttribute.cs | 53 +++ .../Controllers/BaseBillingController.cs | 44 +- src/Api/Billing/Controllers/TaxController.cs | 5 +- .../VNext/AccountBillingVNextController.cs | 64 +++ .../OrganizationBillingVNextController.cs | 107 +++++ .../VNext/ProviderBillingVNextController.cs | 97 +++++ .../Requests/Payment/BillingAddressRequest.cs | 20 + .../Requests/Payment/BitPayCreditRequest.cs | 13 + .../Payment/CheckoutBillingAddressRequest.cs | 24 ++ .../Payment/MinimalBillingAddressRequest.cs | 16 + .../Payment/TokenizedPaymentMethodRequest.cs | 39 ++ .../Payment/VerifyBankAccountRequest.cs | 9 + .../ManageOrganizationBillingRequirement.cs | 18 + src/Api/Utilities/StringMatchesAttribute.cs | 18 + src/Core/Billing/Commands/BillingCommand.cs | 62 +++ .../Billing/Commands/BillingCommandResult.cs | 31 ++ src/Core/Billing/Constants/StripeConstants.cs | 12 +- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Extensions/SubscriberExtensions.cs | 16 +- .../Billing/Models/BillingCommandResult.cs | 36 -- .../Billing/Payment/Clients/BitPayClient.cs | 24 ++ .../CreateBitPayInvoiceForCreditCommand.cs | 59 +++ .../Commands/UpdateBillingAddressCommand.cs | 129 ++++++ .../Commands/UpdatePaymentMethodCommand.cs | 205 +++++++++ .../Commands/VerifyBankAccountCommand.cs | 63 +++ .../Billing/Payment/Models/BillingAddress.cs | 30 ++ .../Payment/Models/MaskedPaymentMethod.cs | 120 ++++++ .../Payment/Models/ProductUsageType.cs | 7 + .../Models/TokenizablePaymentMethodType.cs | 8 + .../Payment/Models/TokenizedPaymentMethod.cs | 8 + .../Payment/Queries/GetBillingAddressQuery.cs | 41 ++ .../Billing/Payment/Queries/GetCreditQuery.cs | 26 ++ .../Payment/Queries/GetPaymentMethodQuery.cs | 96 +++++ src/Core/Billing/Payment/Registrations.cs | 24 ++ .../Pricing/JSON/TypeReadingJsonConverter.cs | 12 +- .../Tax/Commands/PreviewTaxAmountCommand.cs | 172 ++++---- src/Core/Constants.cs | 1 + .../InjectOrganizationAttributeTests.cs | 132 ++++++ .../InjectProviderAttributeTests.cs | 190 +++++++++ .../Attributes/InjectUserAttributesTests.cs | 129 ++++++ .../Billing/Extensions/StripeExtensions.cs | 18 + ...reateBitPayInvoiceForCreditCommandTests.cs | 94 +++++ .../UpdateBillingAddressCommandTests.cs | 349 +++++++++++++++ .../UpdatePaymentMethodCommandTests.cs | 399 ++++++++++++++++++ .../Commands/VerifyBankAccountCommandTests.cs | 81 ++++ .../Models/MaskedPaymentMethodTests.cs | 63 +++ .../Queries/GetBillingAddressQueryTests.cs | 204 +++++++++ .../Payment/Queries/GetCreditQueryTests.cs | 41 ++ .../Queries/GetPaymentMethodQueryTests.cs | 327 ++++++++++++++ .../Commands/PreviewTaxAmountCommandTests.cs | 72 +--- 52 files changed, 3736 insertions(+), 215 deletions(-) create mode 100644 src/Api/Billing/Attributes/InjectOrganizationAttribute.cs create mode 100644 src/Api/Billing/Attributes/InjectProviderAttribute.cs create mode 100644 src/Api/Billing/Attributes/InjectUserAttribute.cs create mode 100644 src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs create mode 100644 src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs create mode 100644 src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/VerifyBankAccountRequest.cs create mode 100644 src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs create mode 100644 src/Api/Utilities/StringMatchesAttribute.cs create mode 100644 src/Core/Billing/Commands/BillingCommand.cs create mode 100644 src/Core/Billing/Commands/BillingCommandResult.cs delete mode 100644 src/Core/Billing/Models/BillingCommandResult.cs create mode 100644 src/Core/Billing/Payment/Clients/BitPayClient.cs create mode 100644 src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs create mode 100644 src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs create mode 100644 src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs create mode 100644 src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs create mode 100644 src/Core/Billing/Payment/Models/BillingAddress.cs create mode 100644 src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs create mode 100644 src/Core/Billing/Payment/Models/ProductUsageType.cs create mode 100644 src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs create mode 100644 src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs create mode 100644 src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs create mode 100644 src/Core/Billing/Payment/Queries/GetCreditQuery.cs create mode 100644 src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs create mode 100644 src/Core/Billing/Payment/Registrations.cs create mode 100644 test/Api.Test/Billing/Attributes/InjectOrganizationAttributeTests.cs create mode 100644 test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs create mode 100644 test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs create mode 100644 test/Core.Test/Billing/Extensions/StripeExtensions.cs create mode 100644 test/Core.Test/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommandTests.cs create mode 100644 test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs create mode 100644 test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs create mode 100644 test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs create mode 100644 test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs create mode 100644 test/Core.Test/Billing/Payment/Queries/GetBillingAddressQueryTests.cs create mode 100644 test/Core.Test/Billing/Payment/Queries/GetCreditQueryTests.cs create mode 100644 test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs diff --git a/src/Api/Billing/Attributes/InjectOrganizationAttribute.cs b/src/Api/Billing/Attributes/InjectOrganizationAttribute.cs new file mode 100644 index 0000000000..f4c2a8c637 --- /dev/null +++ b/src/Api/Billing/Attributes/InjectOrganizationAttribute.cs @@ -0,0 +1,61 @@ +#nullable enable +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.Api; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Bit.Api.Billing.Attributes; + +/// +/// An action filter that facilitates the injection of a parameter into the executing action method arguments. +/// +/// +/// This attribute retrieves the organization associated with the 'organizationId' included in the executing context's route data. If the organization cannot be found, +/// the request is terminated with a not found response. +/// The injected +/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system. +/// +/// +/// EndpointAsync([BindNever] Organization organization) +/// ]]> +/// +/// +public class InjectOrganizationAttribute : ActionFilterAttribute +{ + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + if (!context.RouteData.Values.TryGetValue("organizationId", out var routeValue) || + !Guid.TryParse(routeValue?.ToString(), out var organizationId)) + { + context.Result = new BadRequestObjectResult(new ErrorResponseModel("Route parameter 'organizationId' is missing or invalid.")); + return; + } + + var organizationRepository = context.HttpContext.RequestServices + .GetRequiredService(); + + var organization = await organizationRepository.GetByIdAsync(organizationId); + + if (organization == null) + { + context.Result = new NotFoundObjectResult(new ErrorResponseModel("Organization not found.")); + return; + } + + var organizationParameter = context.ActionDescriptor.Parameters + .FirstOrDefault(p => p.ParameterType == typeof(Organization)); + + if (organizationParameter != null) + { + context.ActionArguments[organizationParameter.Name] = organization; + } + + await next(); + } +} diff --git a/src/Api/Billing/Attributes/InjectProviderAttribute.cs b/src/Api/Billing/Attributes/InjectProviderAttribute.cs new file mode 100644 index 0000000000..e65dda37c3 --- /dev/null +++ b/src/Api/Billing/Attributes/InjectProviderAttribute.cs @@ -0,0 +1,80 @@ +#nullable enable +using Bit.Api.Models.Public.Response; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Bit.Api.Billing.Attributes; + +/// +/// An action filter that facilitates the injection of a parameter into the executing action method arguments after performing an authorization check. +/// +/// +/// This attribute retrieves the provider associated with the 'providerId' included in the executing context's route data. If the provider cannot be found, +/// the request is terminated with a not-found response. It then checks the authorization level for the provider using the provided . +/// If this check fails, the request is terminated with an unauthorized response. +/// The injected +/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system. +/// +/// +/// EndpointAsync([BindNever] Provider provider) +/// ]]> +/// +/// The desired access level for the authorization check. +/// +public class InjectProviderAttribute(ProviderUserType providerUserType) : ActionFilterAttribute +{ + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + if (!context.RouteData.Values.TryGetValue("providerId", out var routeValue) || + !Guid.TryParse(routeValue?.ToString(), out var providerId)) + { + context.Result = new BadRequestObjectResult(new ErrorResponseModel("Route parameter 'providerId' is missing or invalid.")); + return; + } + + var providerRepository = context.HttpContext.RequestServices + .GetRequiredService(); + + var provider = await providerRepository.GetByIdAsync(providerId); + + if (provider == null) + { + context.Result = new NotFoundObjectResult(new ErrorResponseModel("Provider not found.")); + return; + } + + var currentContext = context.HttpContext.RequestServices.GetRequiredService(); + + var unauthorized = providerUserType switch + { + ProviderUserType.ProviderAdmin => !currentContext.ProviderProviderAdmin(providerId), + ProviderUserType.ServiceUser => !currentContext.ProviderUser(providerId), + _ => false + }; + + if (unauthorized) + { + context.Result = new UnauthorizedObjectResult(new ErrorResponseModel("Unauthorized.")); + return; + } + + var providerParameter = context.ActionDescriptor.Parameters + .FirstOrDefault(p => p.ParameterType == typeof(Provider)); + + if (providerParameter != null) + { + context.ActionArguments[providerParameter.Name] = provider; + } + + await next(); + } +} diff --git a/src/Api/Billing/Attributes/InjectUserAttribute.cs b/src/Api/Billing/Attributes/InjectUserAttribute.cs new file mode 100644 index 0000000000..0b614bdc44 --- /dev/null +++ b/src/Api/Billing/Attributes/InjectUserAttribute.cs @@ -0,0 +1,53 @@ +#nullable enable +using Bit.Core.Entities; +using Bit.Core.Models.Api; +using Bit.Core.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Bit.Api.Billing.Attributes; + +/// +/// An action filter that facilitates the injection of a parameter into the executing action method arguments. +/// +/// +/// This attribute retrieves the authorized user associated with the current HTTP context using the service. +/// If the user is unauthorized or cannot be found, the request is terminated with an unauthorized response. +/// The injected +/// parameter must be marked with a [BindNever] attribute to short-circuit the model-binding system. +/// +/// +/// EndpointAsync([BindNever] User user) +/// ]]> +/// +/// +public class InjectUserAttribute : ActionFilterAttribute +{ + public override async Task OnActionExecutionAsync( + ActionExecutingContext context, + ActionExecutionDelegate next) + { + var userService = context.HttpContext.RequestServices.GetRequiredService(); + + var user = await userService.GetUserByPrincipalAsync(context.HttpContext.User); + + if (user == null) + { + context.Result = new UnauthorizedObjectResult(new ErrorResponseModel("Unauthorized.")); + return; + } + + var userParameter = + context.ActionDescriptor.Parameters.FirstOrDefault(parameter => parameter.ParameterType == typeof(User)); + + if (userParameter != null) + { + context.ActionArguments[userParameter.Name] = user; + } + + await next(); + } +} diff --git a/src/Api/Billing/Controllers/BaseBillingController.cs b/src/Api/Billing/Controllers/BaseBillingController.cs index 5f7005fdfc..057c8309fb 100644 --- a/src/Api/Billing/Controllers/BaseBillingController.cs +++ b/src/Api/Billing/Controllers/BaseBillingController.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Api; +#nullable enable +using Bit.Core.Billing.Commands; +using Bit.Core.Models.Api; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -6,20 +8,50 @@ namespace Bit.Api.Billing.Controllers; public abstract class BaseBillingController : Controller { + /// + /// Processes the result of a billing command and converts it to an appropriate HTTP result response. + /// + /// + /// Result to response mappings: + /// + /// : 200 OK + /// : 400 BAD_REQUEST + /// : 409 CONFLICT + /// : 500 INTERNAL_SERVER_ERROR + /// + /// + /// The type of the successful result. + /// The result of executing the billing command. + /// An HTTP result response representing the outcome of the command execution. + protected static IResult Handle(BillingCommandResult result) => + result.Match( + TypedResults.Ok, + badRequest => Error.BadRequest(badRequest.Response), + conflict => Error.Conflict(conflict.Response), + unhandled => Error.ServerError(unhandled.Response, unhandled.Exception)); + protected static class Error { - public static BadRequest BadRequest(Dictionary> errors) => - TypedResults.BadRequest(new ErrorResponseModel(errors)); - public static BadRequest BadRequest(string message) => TypedResults.BadRequest(new ErrorResponseModel(message)); + public static JsonHttpResult Conflict(string message) => + TypedResults.Json( + new ErrorResponseModel(message), + statusCode: StatusCodes.Status409Conflict); + public static NotFound NotFound() => TypedResults.NotFound(new ErrorResponseModel("Resource not found.")); - public static JsonHttpResult ServerError(string message = "Something went wrong with your request. Please contact support.") => + public static JsonHttpResult ServerError( + string message = "Something went wrong with your request. Please contact support for assistance.", + Exception? exception = null) => TypedResults.Json( - new ErrorResponseModel(message), + exception == null ? new ErrorResponseModel(message) : new ErrorResponseModel(message) + { + ExceptionMessage = exception.Message, + ExceptionStackTrace = exception.StackTrace + }, statusCode: StatusCodes.Status500InternalServerError); public static JsonHttpResult Unauthorized(string message = "Unauthorized.") => diff --git a/src/Api/Billing/Controllers/TaxController.cs b/src/Api/Billing/Controllers/TaxController.cs index 7b8b9d960f..d2c1c36726 100644 --- a/src/Api/Billing/Controllers/TaxController.cs +++ b/src/Api/Billing/Controllers/TaxController.cs @@ -28,9 +28,6 @@ public class TaxController( var result = await previewTaxAmountCommand.Run(parameters); - return result.Match( - taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }), - badRequest => Error.BadRequest(badRequest.TranslationKey), - unhandled => Error.ServerError(unhandled.TranslationKey)); + return Handle(result); } } diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs new file mode 100644 index 0000000000..e3b702e36d --- /dev/null +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -0,0 +1,64 @@ +#nullable enable +using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Entities; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Bit.Api.Billing.Controllers.VNext; + +[Authorize("Application")] +[Route("account/billing/vnext")] +[SelfHosted(NotSelfHostedOnly = true)] +public class AccountBillingVNextController( + ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, + IGetCreditQuery getCreditQuery, + IGetPaymentMethodQuery getPaymentMethodQuery, + IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController +{ + [HttpGet("credit")] + [InjectUser] + public async Task GetCreditAsync( + [BindNever] User user) + { + var credit = await getCreditQuery.Run(user); + return TypedResults.Ok(credit); + } + + [HttpPost("credit/bitpay")] + [InjectUser] + public async Task AddCreditViaBitPayAsync( + [BindNever] User user, + [FromBody] BitPayCreditRequest request) + { + var result = await createBitPayInvoiceForCreditCommand.Run( + user, + request.Amount, + request.RedirectUrl); + return Handle(result); + } + + [HttpGet("payment-method")] + [InjectUser] + public async Task GetPaymentMethodAsync( + [BindNever] User user) + { + var paymentMethod = await getPaymentMethodQuery.Run(user); + return TypedResults.Ok(paymentMethod); + } + + [HttpPut("payment-method")] + [InjectUser] + public async Task UpdatePaymentMethodAsync( + [BindNever] User user, + [FromBody] TokenizedPaymentMethodRequest request) + { + var (paymentMethod, billingAddress) = request.ToDomain(); + var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress); + return Handle(result); + } +} diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs new file mode 100644 index 0000000000..429f2065f6 --- /dev/null +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -0,0 +1,107 @@ +#nullable enable +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Api.Billing.Models.Requirements; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +// ReSharper disable RouteTemplates.MethodMissingRouteParameters + +namespace Bit.Api.Billing.Controllers.VNext; + +[Authorize("Application")] +[Route("organizations/{organizationId:guid}/billing/vnext")] +[SelfHosted(NotSelfHostedOnly = true)] +public class OrganizationBillingVNextController( + ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, + IGetBillingAddressQuery getBillingAddressQuery, + IGetCreditQuery getCreditQuery, + IGetPaymentMethodQuery getPaymentMethodQuery, + IUpdateBillingAddressCommand updateBillingAddressCommand, + IUpdatePaymentMethodCommand updatePaymentMethodCommand, + IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController +{ + [Authorize] + [HttpGet("address")] + [InjectOrganization] + public async Task GetBillingAddressAsync( + [BindNever] Organization organization) + { + var billingAddress = await getBillingAddressQuery.Run(organization); + return TypedResults.Ok(billingAddress); + } + + [Authorize] + [HttpPut("address")] + [InjectOrganization] + public async Task UpdateBillingAddressAsync( + [BindNever] Organization organization, + [FromBody] BillingAddressRequest request) + { + var billingAddress = request.ToDomain(); + var result = await updateBillingAddressCommand.Run(organization, billingAddress); + return Handle(result); + } + + [Authorize] + [HttpGet("credit")] + [InjectOrganization] + public async Task GetCreditAsync( + [BindNever] Organization organization) + { + var credit = await getCreditQuery.Run(organization); + return TypedResults.Ok(credit); + } + + [Authorize] + [HttpPost("credit/bitpay")] + [InjectOrganization] + public async Task AddCreditViaBitPayAsync( + [BindNever] Organization organization, + [FromBody] BitPayCreditRequest request) + { + var result = await createBitPayInvoiceForCreditCommand.Run( + organization, + request.Amount, + request.RedirectUrl); + return Handle(result); + } + + [Authorize] + [HttpGet("payment-method")] + [InjectOrganization] + public async Task GetPaymentMethodAsync( + [BindNever] Organization organization) + { + var paymentMethod = await getPaymentMethodQuery.Run(organization); + return TypedResults.Ok(paymentMethod); + } + + [Authorize] + [HttpPut("payment-method")] + [InjectOrganization] + public async Task UpdatePaymentMethodAsync( + [BindNever] Organization organization, + [FromBody] TokenizedPaymentMethodRequest request) + { + var (paymentMethod, billingAddress) = request.ToDomain(); + var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, billingAddress); + return Handle(result); + } + + [Authorize] + [HttpPost("payment-method/verify-bank-account")] + [InjectOrganization] + public async Task VerifyBankAccountAsync( + [BindNever] Organization organization, + [FromBody] VerifyBankAccountRequest request) + { + var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode); + return Handle(result); + } +} diff --git a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs new file mode 100644 index 0000000000..be7963236f --- /dev/null +++ b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs @@ -0,0 +1,97 @@ +#nullable enable +using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +// ReSharper disable RouteTemplates.MethodMissingRouteParameters + +namespace Bit.Api.Billing.Controllers.VNext; + +[Route("providers/{providerId:guid}/billing/vnext")] +[SelfHosted(NotSelfHostedOnly = true)] +public class ProviderBillingVNextController( + ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, + IGetBillingAddressQuery getBillingAddressQuery, + IGetCreditQuery getCreditQuery, + IGetPaymentMethodQuery getPaymentMethodQuery, + IUpdateBillingAddressCommand updateBillingAddressCommand, + IUpdatePaymentMethodCommand updatePaymentMethodCommand, + IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController +{ + [HttpGet("address")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task GetBillingAddressAsync( + [BindNever] Provider provider) + { + var billingAddress = await getBillingAddressQuery.Run(provider); + return TypedResults.Ok(billingAddress); + } + + [HttpPut("address")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task UpdateBillingAddressAsync( + [BindNever] Provider provider, + [FromBody] BillingAddressRequest request) + { + var billingAddress = request.ToDomain(); + var result = await updateBillingAddressCommand.Run(provider, billingAddress); + return Handle(result); + } + + [HttpGet("credit")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task GetCreditAsync( + [BindNever] Provider provider) + { + var credit = await getCreditQuery.Run(provider); + return TypedResults.Ok(credit); + } + + [HttpPost("credit/bitpay")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task AddCreditViaBitPayAsync( + [BindNever] Provider provider, + [FromBody] BitPayCreditRequest request) + { + var result = await createBitPayInvoiceForCreditCommand.Run( + provider, + request.Amount, + request.RedirectUrl); + return Handle(result); + } + + [HttpGet("payment-method")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task GetPaymentMethodAsync( + [BindNever] Provider provider) + { + var paymentMethod = await getPaymentMethodQuery.Run(provider); + return TypedResults.Ok(paymentMethod); + } + + [HttpPut("payment-method")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task UpdatePaymentMethodAsync( + [BindNever] Provider provider, + [FromBody] TokenizedPaymentMethodRequest request) + { + var (paymentMethod, billingAddress) = request.ToDomain(); + var result = await updatePaymentMethodCommand.Run(provider, paymentMethod, billingAddress); + return Handle(result); + } + + [HttpPost("payment-method/verify-bank-account")] + [InjectProvider(ProviderUserType.ProviderAdmin)] + public async Task VerifyBankAccountAsync( + [BindNever] Provider provider, + [FromBody] VerifyBankAccountRequest request) + { + var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode); + return Handle(result); + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs new file mode 100644 index 0000000000..5c3c47f585 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs @@ -0,0 +1,20 @@ +#nullable enable +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public record BillingAddressRequest : CheckoutBillingAddressRequest +{ + public string? Line1 { get; set; } + public string? Line2 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + + public override BillingAddress ToDomain() => base.ToDomain() with + { + Line1 = Line1, + Line2 = Line2, + City = City, + State = State, + }; +} diff --git a/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs b/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs new file mode 100644 index 0000000000..bb6e7498d7 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public record BitPayCreditRequest +{ + [Required] + public required decimal Amount { get; set; } + + [Required] + public required string RedirectUrl { get; set; } = null!; +} diff --git a/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs new file mode 100644 index 0000000000..54116e897d --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs @@ -0,0 +1,24 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public record CheckoutBillingAddressRequest : MinimalBillingAddressRequest +{ + public TaxIdRequest? TaxId { get; set; } + + public override BillingAddress ToDomain() => base.ToDomain() with + { + TaxId = TaxId != null ? new TaxID(TaxId.Code, TaxId.Value) : null + }; + + public class TaxIdRequest + { + [Required] + public string Code { get; set; } = null!; + + [Required] + public string Value { get; set; } = null!; + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs new file mode 100644 index 0000000000..b4d28017d5 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs @@ -0,0 +1,16 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public record MinimalBillingAddressRequest +{ + [Required] + [StringLength(2, MinimumLength = 2, ErrorMessage = "Country code must be 2 characters long.")] + public required string Country { get; set; } = null!; + [Required] + public required string PostalCode { get; set; } = null!; + + public virtual BillingAddress ToDomain() => new() { Country = Country, PostalCode = PostalCode, }; +} diff --git a/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs new file mode 100644 index 0000000000..663e4e7cd2 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs @@ -0,0 +1,39 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Api.Utilities; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public class TokenizedPaymentMethodRequest +{ + [Required] + [StringMatches("bankAccount", "card", "payPal", + ErrorMessage = "Payment method type must be one of: bankAccount, card, payPal")] + public required string Type { get; set; } + + [Required] + public required string Token { get; set; } + + public MinimalBillingAddressRequest? BillingAddress { get; set; } + + public (TokenizedPaymentMethod, BillingAddress?) ToDomain() + { + var paymentMethod = new TokenizedPaymentMethod + { + Type = Type switch + { + "bankAccount" => TokenizablePaymentMethodType.BankAccount, + "card" => TokenizablePaymentMethodType.Card, + "payPal" => TokenizablePaymentMethodType.PayPal, + _ => throw new InvalidOperationException( + $"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}") + }, + Token = Token + }; + + var billingAddress = BillingAddress?.ToDomain(); + + return (paymentMethod, billingAddress); + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/VerifyBankAccountRequest.cs b/src/Api/Billing/Models/Requests/Payment/VerifyBankAccountRequest.cs new file mode 100644 index 0000000000..2b5d6a0cb1 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/VerifyBankAccountRequest.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public class VerifyBankAccountRequest +{ + [Required] + public required string DescriptorCode { get; set; } +} diff --git a/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs b/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs new file mode 100644 index 0000000000..4efdf0812a --- /dev/null +++ b/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs @@ -0,0 +1,18 @@ +#nullable enable +using Bit.Api.AdminConsole.Authorization; +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.Billing.Models.Requirements; + +public class ManageOrganizationBillingRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/Utilities/StringMatchesAttribute.cs b/src/Api/Utilities/StringMatchesAttribute.cs new file mode 100644 index 0000000000..28485aed40 --- /dev/null +++ b/src/Api/Utilities/StringMatchesAttribute.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Utilities; + +public class StringMatchesAttribute(params string[]? accepted) : ValidationAttribute +{ + public override bool IsValid(object? value) + { + if (value is not string str || + accepted == null || + accepted.Length == 0) + { + return false; + } + + return accepted.Contains(str); + } +} diff --git a/src/Core/Billing/Commands/BillingCommand.cs b/src/Core/Billing/Commands/BillingCommand.cs new file mode 100644 index 0000000000..e6c6375b62 --- /dev/null +++ b/src/Core/Billing/Commands/BillingCommand.cs @@ -0,0 +1,62 @@ +using Bit.Core.Billing.Constants; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Commands; + +using static StripeConstants; + +public abstract class BillingCommand( + ILogger logger) +{ + protected string CommandName => GetType().Name; + + /// + /// Executes the provided function within a predefined execution context, handling any exceptions that occur during the process. + /// + /// The type of the successful result expected from the provided function. + /// A function that performs an operation and returns a . + /// A task that represents the operation. The result provides a which may indicate success or an error outcome. + protected async Task> HandleAsync( + Func>> function) + { + try + { + return await function(); + } + catch (StripeException stripeException) when (ErrorCodes.Get().Contains(stripeException.StripeError.Code)) + { + return stripeException.StripeError.Code switch + { + ErrorCodes.CustomerTaxLocationInvalid => + new BadRequest("Your location wasn't recognized. Please ensure your country and postal code are valid and try again."), + + ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => + new BadRequest("You have exceeded the number of allowed verification attempts. Please contact support for assistance."), + + ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => + new BadRequest("The verification code you provided does not match the one sent to your bank account. Please try again."), + + ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => + new BadRequest("Your bank account was not verified within the required time period. Please contact support for assistance."), + + ErrorCodes.TaxIdInvalid => + new BadRequest("The tax ID number you provided was invalid. Please try again or contact support for assistance."), + + _ => new Unhandled(stripeException) + }; + } + catch (StripeException stripeException) + { + logger.LogError(stripeException, + "{Command}: An error occurred while communicating with Stripe | Code = {Code}", CommandName, + stripeException.StripeError.Code); + return new Unhandled(stripeException); + } + catch (Exception exception) + { + logger.LogError(exception, "{Command}: An unknown error occurred during execution", CommandName); + return new Unhandled(exception); + } + } +} diff --git a/src/Core/Billing/Commands/BillingCommandResult.cs b/src/Core/Billing/Commands/BillingCommandResult.cs new file mode 100644 index 0000000000..b69ad4bf12 --- /dev/null +++ b/src/Core/Billing/Commands/BillingCommandResult.cs @@ -0,0 +1,31 @@ +#nullable enable +using OneOf; + +namespace Bit.Core.Billing.Commands; + +public record BadRequest(string Response); +public record Conflict(string Response); +public record Unhandled(Exception? Exception = null, string Response = "Something went wrong with your request. Please contact support for assistance."); + +/// +/// A union type representing the result of a billing command. +/// +/// Choices include: +/// +/// : Success +/// : Invalid input +/// : A known, but unresolvable issue +/// : An unknown issue +/// +/// +/// +/// The successful result type of the operation. +public class BillingCommandResult : OneOfBase +{ + private BillingCommandResult(OneOf input) : base(input) { } + + public static implicit operator BillingCommandResult(T output) => new(output); + public static implicit operator BillingCommandResult(BadRequest badRequest) => new(badRequest); + public static implicit operator BillingCommandResult(Conflict conflict) => new(conflict); + public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); +} diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 0cffad72d3..3aaa519d66 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Constants; +using System.Reflection; + +namespace Bit.Core.Billing.Constants; public static class StripeConstants { @@ -36,6 +38,13 @@ public static class StripeConstants public const string PaymentMethodMicroDepositVerificationDescriptorCodeMismatch = "payment_method_microdeposit_verification_descriptor_code_mismatch"; public const string PaymentMethodMicroDepositVerificationTimeout = "payment_method_microdeposit_verification_timeout"; public const string TaxIdInvalid = "tax_id_invalid"; + + public static string[] Get() => + typeof(ErrorCodes) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi is { IsLiteral: true, IsInitOnly: false } && fi.FieldType == typeof(string)) + .Select(fi => (string)fi.GetValue(null)!) + .ToArray(); } public static class InvoiceStatus @@ -51,6 +60,7 @@ public static class StripeConstants public const string InvoiceApproved = "invoice_approved"; public const string OrganizationId = "organizationId"; public const string ProviderId = "providerId"; + public const string RetiredBraintreeCustomerId = "btCustomerId_old"; public const string UserId = "userId"; } diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 5c7a42e9b8..5f1a0668bd 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Payment; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; @@ -27,5 +28,6 @@ public static class ServiceCollectionExtensions services.AddLicenseServices(); services.AddPricingClient(); services.AddTransient(); + services.AddPaymentOperations(); } } diff --git a/src/Core/Billing/Extensions/SubscriberExtensions.cs b/src/Core/Billing/Extensions/SubscriberExtensions.cs index e322ed7317..fc804de224 100644 --- a/src/Core/Billing/Extensions/SubscriberExtensions.cs +++ b/src/Core/Billing/Extensions/SubscriberExtensions.cs @@ -1,4 +1,8 @@ -using Bit.Core.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Entities; namespace Bit.Core.Billing.Extensions; @@ -23,4 +27,14 @@ public static class SubscriberExtensions ? subscriberName : subscriberName[..30]; } + + public static ProductUsageType GetProductUsageType(this ISubscriber subscriber) + => subscriber switch + { + User => ProductUsageType.Personal, + Organization organization when organization.PlanType.GetProductTier() is ProductTierType.Free or ProductTierType.Families => ProductUsageType.Personal, + Organization => ProductUsageType.Business, + Provider => ProductUsageType.Business, + _ => throw new ArgumentOutOfRangeException(nameof(subscriber)) + }; } diff --git a/src/Core/Billing/Models/BillingCommandResult.cs b/src/Core/Billing/Models/BillingCommandResult.cs deleted file mode 100644 index 1b8eefe8df..0000000000 --- a/src/Core/Billing/Models/BillingCommandResult.cs +++ /dev/null @@ -1,36 +0,0 @@ -using OneOf; - -namespace Bit.Core.Billing.Models; - -public record BadRequest(string TranslationKey) -{ - public static BadRequest TaxIdNumberInvalid => new(BillingErrorTranslationKeys.TaxIdInvalid); - public static BadRequest TaxLocationInvalid => new(BillingErrorTranslationKeys.CustomerTaxLocationInvalid); - public static BadRequest UnknownTaxIdType => new(BillingErrorTranslationKeys.UnknownTaxIdType); -} - -public record Unhandled(string TranslationKey = BillingErrorTranslationKeys.UnhandledError); - -public class BillingCommandResult : OneOfBase -{ - private BillingCommandResult(OneOf input) : base(input) { } - - public static implicit operator BillingCommandResult(T output) => new(output); - public static implicit operator BillingCommandResult(BadRequest badRequest) => new(badRequest); - public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); -} - -public static class BillingErrorTranslationKeys -{ - // "The tax ID number you provided was invalid. Please try again or contact support." - public const string TaxIdInvalid = "taxIdInvalid"; - - // "Your location wasn't recognized. Please ensure your country and postal code are valid and try again." - public const string CustomerTaxLocationInvalid = "customerTaxLocationInvalid"; - - // "Something went wrong with your request. Please contact support." - public const string UnhandledError = "unhandledBillingError"; - - // "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support." - public const string UnknownTaxIdType = "unknownTaxIdType"; -} diff --git a/src/Core/Billing/Payment/Clients/BitPayClient.cs b/src/Core/Billing/Payment/Clients/BitPayClient.cs new file mode 100644 index 0000000000..2cb8fb66ef --- /dev/null +++ b/src/Core/Billing/Payment/Clients/BitPayClient.cs @@ -0,0 +1,24 @@ +using Bit.Core.Settings; +using BitPayLight; +using BitPayLight.Models.Invoice; + +namespace Bit.Core.Billing.Payment.Clients; + +public interface IBitPayClient +{ + Task GetInvoice(string invoiceId); + Task CreateInvoice(Invoice invoice); +} + +public class BitPayClient( + GlobalSettings globalSettings) : IBitPayClient +{ + private readonly BitPay _bitPay = new( + globalSettings.BitPay.Token, globalSettings.BitPay.Production ? Env.Prod : Env.Test); + + public Task GetInvoice(string invoiceId) + => _bitPay.GetInvoice(invoiceId); + + public Task CreateInvoice(Invoice invoice) + => _bitPay.CreateInvoice(invoice); +} diff --git a/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs b/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs new file mode 100644 index 0000000000..f61fa9d0f9 --- /dev/null +++ b/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs @@ -0,0 +1,59 @@ +#nullable enable +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Payment.Clients; +using Bit.Core.Entities; +using Bit.Core.Settings; +using BitPayLight.Models.Invoice; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Billing.Payment.Commands; + +public interface ICreateBitPayInvoiceForCreditCommand +{ + Task> Run( + ISubscriber subscriber, + decimal amount, + string redirectUrl); +} + +public class CreateBitPayInvoiceForCreditCommand( + IBitPayClient bitPayClient, + GlobalSettings globalSettings, + ILogger logger) : BillingCommand(logger), ICreateBitPayInvoiceForCreditCommand +{ + public Task> Run( + ISubscriber subscriber, + decimal amount, + string redirectUrl) => HandleAsync(async () => + { + var (name, email, posData) = GetSubscriberInformation(subscriber); + + var invoice = new Invoice + { + Buyer = new Buyer { Email = email, Name = name }, + Currency = "USD", + ExtendedNotifications = true, + FullNotifications = true, + ItemDesc = "Bitwarden", + NotificationUrl = globalSettings.BitPay.NotificationUrl, + PosData = posData, + Price = Convert.ToDouble(amount), + RedirectUrl = redirectUrl + }; + + var created = await bitPayClient.CreateInvoice(invoice); + return created.Url; + }); + + private static (string? Name, string? Email, string POSData) GetSubscriberInformation( + ISubscriber subscriber) => subscriber switch + { + User user => (user.Email, user.Email, $"userId:{user.Id},accountCredit:1"), + Organization organization => (organization.Name, organization.BillingEmail, + $"organizationId:{organization.Id},accountCredit:1"), + Provider provider => (provider.Name, provider.BillingEmail, $"providerId:{provider.Id},accountCredit:1"), + _ => throw new ArgumentOutOfRangeException(nameof(subscriber)) + }; +} diff --git a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs new file mode 100644 index 0000000000..adc534bd7d --- /dev/null +++ b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs @@ -0,0 +1,129 @@ +#nullable enable +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Entities; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Payment.Commands; + +public interface IUpdateBillingAddressCommand +{ + Task> Run( + ISubscriber subscriber, + BillingAddress billingAddress); +} + +public class UpdateBillingAddressCommand( + ILogger logger, + IStripeAdapter stripeAdapter) : BillingCommand(logger), IUpdateBillingAddressCommand +{ + public Task> Run( + ISubscriber subscriber, + BillingAddress billingAddress) => HandleAsync(() => subscriber.GetProductUsageType() switch + { + ProductUsageType.Personal => UpdatePersonalBillingAddressAsync(subscriber, billingAddress), + ProductUsageType.Business => UpdateBusinessBillingAddressAsync(subscriber, billingAddress) + }); + + private async Task> UpdatePersonalBillingAddressAsync( + ISubscriber subscriber, + BillingAddress billingAddress) + { + var customer = + await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, + new CustomerUpdateOptions + { + Address = new AddressOptions + { + Country = billingAddress.Country, + PostalCode = billingAddress.PostalCode, + Line1 = billingAddress.Line1, + Line2 = billingAddress.Line2, + City = billingAddress.City, + State = billingAddress.State + }, + Expand = ["subscriptions"] + }); + + await EnableAutomaticTaxAsync(subscriber, customer); + + return BillingAddress.From(customer.Address); + } + + private async Task> UpdateBusinessBillingAddressAsync( + ISubscriber subscriber, + BillingAddress billingAddress) + { + var customer = + await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, + new CustomerUpdateOptions + { + Address = new AddressOptions + { + Country = billingAddress.Country, + PostalCode = billingAddress.PostalCode, + Line1 = billingAddress.Line1, + Line2 = billingAddress.Line2, + City = billingAddress.City, + State = billingAddress.State + }, + Expand = ["subscriptions", "tax_ids"], + TaxExempt = billingAddress.Country != "US" + ? StripeConstants.TaxExempt.Reverse + : StripeConstants.TaxExempt.None + }); + + await EnableAutomaticTaxAsync(subscriber, customer); + + var deleteExistingTaxIds = customer.TaxIds?.Any() ?? false + ? customer.TaxIds.Select(taxId => stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id)).ToList() + : []; + + if (billingAddress.TaxId == null) + { + await Task.WhenAll(deleteExistingTaxIds); + return BillingAddress.From(customer.Address); + } + + var updatedTaxId = await stripeAdapter.TaxIdCreateAsync(customer.Id, + new TaxIdCreateOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }); + + if (billingAddress.TaxId.Code == StripeConstants.TaxIdType.SpanishNIF) + { + updatedTaxId = await stripeAdapter.TaxIdCreateAsync(customer.Id, + new TaxIdCreateOptions + { + Type = StripeConstants.TaxIdType.EUVAT, + Value = $"ES{billingAddress.TaxId.Value}" + }); + } + + await Task.WhenAll(deleteExistingTaxIds); + + return BillingAddress.From(customer.Address, updatedTaxId); + } + + private async Task EnableAutomaticTaxAsync( + ISubscriber subscriber, + Customer customer) + { + if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) + { + var subscription = customer.Subscriptions.FirstOrDefault(subscription => + subscription.Id == subscriber.GatewaySubscriptionId); + + if (subscription is { AutomaticTax.Enabled: false }) + { + await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + } + } +} diff --git a/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs b/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs new file mode 100644 index 0000000000..cda685d520 --- /dev/null +++ b/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs @@ -0,0 +1,205 @@ +#nullable enable +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Braintree; +using Microsoft.Extensions.Logging; +using Stripe; +using Customer = Stripe.Customer; + +namespace Bit.Core.Billing.Payment.Commands; + +public interface IUpdatePaymentMethodCommand +{ + Task> Run( + ISubscriber subscriber, + TokenizedPaymentMethod paymentMethod, + BillingAddress? billingAddress); +} + +public class UpdatePaymentMethodCommand( + IBraintreeGateway braintreeGateway, + IGlobalSettings globalSettings, + ILogger logger, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : BillingCommand(logger), IUpdatePaymentMethodCommand +{ + private readonly ILogger _logger = logger; + private static readonly Conflict _conflict = new("We had a problem updating your payment method. Please contact support for assistance."); + + public Task> Run( + ISubscriber subscriber, + TokenizedPaymentMethod paymentMethod, + BillingAddress? billingAddress) => HandleAsync(async () => + { + var customer = await subscriberService.GetCustomer(subscriber); + + var result = paymentMethod.Type switch + { + TokenizablePaymentMethodType.BankAccount => await AddBankAccountAsync(subscriber, customer, paymentMethod.Token), + TokenizablePaymentMethodType.Card => await AddCardAsync(customer, paymentMethod.Token), + TokenizablePaymentMethodType.PayPal => await AddPayPalAsync(subscriber, customer, paymentMethod.Token), + _ => new BadRequest($"Payment method type '{paymentMethod.Type}' is not supported.") + }; + + if (billingAddress != null && customer.Address is not { Country: not null, PostalCode: not null }) + { + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + Address = new AddressOptions + { + Country = billingAddress.Country, + PostalCode = billingAddress.PostalCode + } + }); + } + + return result; + }); + + private async Task> AddBankAccountAsync( + ISubscriber subscriber, + Customer customer, + string token) + { + var setupIntents = await stripeAdapter.SetupIntentList(new SetupIntentListOptions + { + Expand = ["data.payment_method"], + PaymentMethod = token + }); + + switch (setupIntents.Count) + { + case 0: + _logger.LogError("{Command}: Could not find setup intent for subscriber's ({SubscriberID}) bank account", CommandName, subscriber.Id); + return _conflict; + case > 1: + _logger.LogError("{Command}: Found more than one set up intent for subscriber's ({SubscriberID}) bank account", CommandName, subscriber.Id); + return _conflict; + } + + var setupIntent = setupIntents.First(); + + await setupIntentCache.Set(subscriber.Id, setupIntent.Id); + + await UnlinkBraintreeCustomerAsync(customer); + + return MaskedPaymentMethod.From(setupIntent); + } + + private async Task> AddCardAsync( + Customer customer, + string token) + { + var paymentMethod = await stripeAdapter.PaymentMethodAttachAsync(token, new PaymentMethodAttachOptions { Customer = customer.Id }); + + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = token } + }); + + await UnlinkBraintreeCustomerAsync(customer); + + return MaskedPaymentMethod.From(paymentMethod.Card); + } + + private async Task> AddPayPalAsync( + ISubscriber subscriber, + Customer customer, + string token) + { + Braintree.Customer braintreeCustomer; + + if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId)) + { + braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId); + + await ReplaceBraintreePaymentMethodAsync(braintreeCustomer, token); + } + else + { + braintreeCustomer = await CreateBraintreeCustomerAsync(subscriber, token); + + var metadata = new Dictionary + { + [StripeConstants.MetadataKeys.BraintreeCustomerId] = braintreeCustomer.Id + }; + + await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { Metadata = metadata }); + } + + var payPalAccount = braintreeCustomer.DefaultPaymentMethod as PayPalAccount; + + return MaskedPaymentMethod.From(payPalAccount!); + } + + private async Task CreateBraintreeCustomerAsync( + ISubscriber subscriber, + string token) + { + var braintreeCustomerId = + subscriber.BraintreeCustomerIdPrefix() + + subscriber.Id.ToString("N").ToLower() + + CoreHelpers.RandomString(3, upper: false, numeric: false); + + var result = await braintreeGateway.Customer.CreateAsync(new CustomerRequest + { + Id = braintreeCustomerId, + CustomFields = new Dictionary + { + [subscriber.BraintreeIdField()] = subscriber.Id.ToString(), + [subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion + }, + Email = subscriber.BillingEmailAddress(), + PaymentMethodNonce = token + }); + + return result.Target; + } + + private async Task ReplaceBraintreePaymentMethodAsync( + Braintree.Customer customer, + string token) + { + var existing = customer.DefaultPaymentMethod; + + var result = await braintreeGateway.PaymentMethod.CreateAsync(new PaymentMethodRequest + { + CustomerId = customer.Id, + PaymentMethodNonce = token + }); + + await braintreeGateway.Customer.UpdateAsync( + customer.Id, + new CustomerRequest { DefaultPaymentMethodToken = result.Target.Token }); + + if (existing != null) + { + await braintreeGateway.PaymentMethod.DeleteAsync(existing.Token); + } + } + + private async Task UnlinkBraintreeCustomerAsync( + Customer customer) + { + if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId)) + { + var metadata = new Dictionary + { + [StripeConstants.MetadataKeys.RetiredBraintreeCustomerId] = braintreeCustomerId, + [StripeConstants.MetadataKeys.BraintreeCustomerId] = string.Empty + }; + + await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { Metadata = metadata }); + } + } +} diff --git a/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs b/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs new file mode 100644 index 0000000000..1e9492b876 --- /dev/null +++ b/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs @@ -0,0 +1,63 @@ +#nullable enable +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Entities; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Payment.Commands; + +public interface IVerifyBankAccountCommand +{ + Task> Run( + ISubscriber subscriber, + string descriptorCode); +} + +public class VerifyBankAccountCommand( + ILogger logger, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter) : BillingCommand(logger), IVerifyBankAccountCommand +{ + private readonly ILogger _logger = logger; + + private static readonly Conflict _conflict = + new("We had a problem verifying your bank account. Please contact support for assistance."); + + public Task> Run( + ISubscriber subscriber, + string descriptorCode) => HandleAsync(async () => + { + var setupIntentId = await setupIntentCache.Get(subscriber.Id); + + if (string.IsNullOrEmpty(setupIntentId)) + { + _logger.LogError( + "{Command}: Could not find setup intent to verify subscriber's ({SubscriberID}) bank account", + CommandName, subscriber.Id); + return _conflict; + } + + await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, + new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode }); + + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, + new SetupIntentGetOptions { Expand = ["payment_method"] }); + + var paymentMethod = await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, + new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); + + await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, + new CustomerUpdateOptions + { + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + DefaultPaymentMethod = setupIntent.PaymentMethodId + } + }); + + return MaskedPaymentMethod.From(paymentMethod.UsBankAccount); + }); +} diff --git a/src/Core/Billing/Payment/Models/BillingAddress.cs b/src/Core/Billing/Payment/Models/BillingAddress.cs new file mode 100644 index 0000000000..5c2c43231c --- /dev/null +++ b/src/Core/Billing/Payment/Models/BillingAddress.cs @@ -0,0 +1,30 @@ +#nullable enable +using Stripe; + +namespace Bit.Core.Billing.Payment.Models; + +public record TaxID(string Code, string Value); + +public record BillingAddress +{ + public required string Country { get; set; } + public required string PostalCode { get; set; } + public string? Line1 { get; set; } + public string? Line2 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public TaxID? TaxId { get; set; } + + public static BillingAddress From(Address address) => new() + { + Country = address.Country, + PostalCode = address.PostalCode, + Line1 = address.Line1, + Line2 = address.Line2, + City = address.City, + State = address.State + }; + + public static BillingAddress From(Address address, TaxId? taxId) => + From(address) with { TaxId = taxId != null ? new TaxID(taxId.Type, taxId.Value) : null }; +} diff --git a/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs new file mode 100644 index 0000000000..c98fddc785 --- /dev/null +++ b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs @@ -0,0 +1,120 @@ +#nullable enable +using System.Text.Json; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Pricing.JSON; +using Braintree; +using OneOf; +using Stripe; + +namespace Bit.Core.Billing.Payment.Models; + +public record MaskedBankAccount +{ + public required string BankName { get; init; } + public required string Last4 { get; init; } + public required bool Verified { get; init; } + public string Type => "bankAccount"; +} + +public record MaskedCard +{ + public required string Brand { get; init; } + public required string Last4 { get; init; } + public required string Expiration { get; init; } + public string Type => "card"; +} + +public record MaskedPayPalAccount +{ + public required string Email { get; init; } + public string Type => "payPal"; +} + +[JsonConverter(typeof(MaskedPaymentMethodJsonConverter))] +public class MaskedPaymentMethod(OneOf input) + : OneOfBase(input) +{ + public static implicit operator MaskedPaymentMethod(MaskedBankAccount bankAccount) => new(bankAccount); + public static implicit operator MaskedPaymentMethod(MaskedCard card) => new(card); + public static implicit operator MaskedPaymentMethod(MaskedPayPalAccount payPal) => new(payPal); + + public static MaskedPaymentMethod From(BankAccount bankAccount) => new MaskedBankAccount + { + BankName = bankAccount.BankName, + Last4 = bankAccount.Last4, + Verified = bankAccount.Status == "verified" + }; + + public static MaskedPaymentMethod From(Card card) => new MaskedCard + { + Brand = card.Brand.ToLower(), + Last4 = card.Last4, + Expiration = $"{card.ExpMonth:00}/{card.ExpYear}" + }; + + public static MaskedPaymentMethod From(PaymentMethodCard card) => new MaskedCard + { + Brand = card.Brand.ToLower(), + Last4 = card.Last4, + Expiration = $"{card.ExpMonth:00}/{card.ExpYear}" + }; + + public static MaskedPaymentMethod From(SetupIntent setupIntent) => new MaskedBankAccount + { + BankName = setupIntent.PaymentMethod.UsBankAccount.BankName, + Last4 = setupIntent.PaymentMethod.UsBankAccount.Last4, + Verified = false + }; + + public static MaskedPaymentMethod From(SourceCard sourceCard) => new MaskedCard + { + Brand = sourceCard.Brand.ToLower(), + Last4 = sourceCard.Last4, + Expiration = $"{sourceCard.ExpMonth:00}/{sourceCard.ExpYear}" + }; + + public static MaskedPaymentMethod From(PaymentMethodUsBankAccount bankAccount) => new MaskedBankAccount + { + BankName = bankAccount.BankName, + Last4 = bankAccount.Last4, + Verified = true + }; + + public static MaskedPaymentMethod From(PayPalAccount payPalAccount) => new MaskedPayPalAccount { Email = payPalAccount.Email }; +} + +public class MaskedPaymentMethodJsonConverter : TypeReadingJsonConverter +{ + protected override string TypePropertyName => nameof(MaskedBankAccount.Type).ToLower(); + + public override MaskedPaymentMethod? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var type = ReadType(reader); + + return type switch + { + "bankAccount" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var bankAccount => new MaskedPaymentMethod(bankAccount) + }, + "card" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var card => new MaskedPaymentMethod(card) + }, + "payPal" => JsonSerializer.Deserialize(ref reader, options) switch + { + null => null, + var payPal => new MaskedPaymentMethod(payPal) + }, + _ => Skip(ref reader) + }; + } + + public override void Write(Utf8JsonWriter writer, MaskedPaymentMethod value, JsonSerializerOptions options) + => value.Switch( + bankAccount => JsonSerializer.Serialize(writer, bankAccount, options), + card => JsonSerializer.Serialize(writer, card, options), + payPal => JsonSerializer.Serialize(writer, payPal, options)); +} diff --git a/src/Core/Billing/Payment/Models/ProductUsageType.cs b/src/Core/Billing/Payment/Models/ProductUsageType.cs new file mode 100644 index 0000000000..2ecd1233c6 --- /dev/null +++ b/src/Core/Billing/Payment/Models/ProductUsageType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Payment.Models; + +public enum ProductUsageType +{ + Personal, + Business +} diff --git a/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs b/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs new file mode 100644 index 0000000000..d27a924360 --- /dev/null +++ b/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Billing.Payment.Models; + +public enum TokenizablePaymentMethodType +{ + BankAccount, + Card, + PayPal +} diff --git a/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs b/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs new file mode 100644 index 0000000000..edbf1bb121 --- /dev/null +++ b/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace Bit.Core.Billing.Payment.Models; + +public record TokenizedPaymentMethod +{ + public required TokenizablePaymentMethodType Type { get; set; } + public required string Token { get; set; } +} diff --git a/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs b/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs new file mode 100644 index 0000000000..84d4d4f377 --- /dev/null +++ b/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs @@ -0,0 +1,41 @@ +#nullable enable +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Stripe; + +namespace Bit.Core.Billing.Payment.Queries; + +public interface IGetBillingAddressQuery +{ + Task Run(ISubscriber subscriber); +} + +public class GetBillingAddressQuery( + ISubscriberService subscriberService) : IGetBillingAddressQuery +{ + public async Task Run(ISubscriber subscriber) + { + var productUsageType = subscriber.GetProductUsageType(); + + var options = productUsageType switch + { + ProductUsageType.Business => new CustomerGetOptions { Expand = ["tax_ids"] }, + _ => new CustomerGetOptions() + }; + + var customer = await subscriberService.GetCustomer(subscriber, options); + + if (customer is not { Address: { Country: not null, PostalCode: not null } }) + { + return null; + } + + var taxId = productUsageType == ProductUsageType.Business ? customer.TaxIds?.FirstOrDefault() : null; + + return taxId != null + ? BillingAddress.From(customer.Address, taxId) + : BillingAddress.From(customer.Address); + } +} diff --git a/src/Core/Billing/Payment/Queries/GetCreditQuery.cs b/src/Core/Billing/Payment/Queries/GetCreditQuery.cs new file mode 100644 index 0000000000..79c9a13aba --- /dev/null +++ b/src/Core/Billing/Payment/Queries/GetCreditQuery.cs @@ -0,0 +1,26 @@ +#nullable enable +using Bit.Core.Billing.Services; +using Bit.Core.Entities; + +namespace Bit.Core.Billing.Payment.Queries; + +public interface IGetCreditQuery +{ + Task Run(ISubscriber subscriber); +} + +public class GetCreditQuery( + ISubscriberService subscriberService) : IGetCreditQuery +{ + public async Task Run(ISubscriber subscriber) + { + var customer = await subscriberService.GetCustomer(subscriber); + + if (customer == null) + { + return null; + } + + return Convert.ToDecimal(customer.Balance) * -1 / 100; + } +} diff --git a/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs new file mode 100644 index 0000000000..eb42a8c78a --- /dev/null +++ b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs @@ -0,0 +1,96 @@ +#nullable enable +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.Services; +using Bit.Core.Entities; +using Bit.Core.Services; +using Braintree; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Payment.Queries; + +public interface IGetPaymentMethodQuery +{ + Task Run(ISubscriber subscriber); +} + +public class GetPaymentMethodQuery( + IBraintreeGateway braintreeGateway, + ILogger logger, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : IGetPaymentMethodQuery +{ + public async Task Run(ISubscriber subscriber) + { + var customer = await subscriberService.GetCustomer(subscriber, + new CustomerGetOptions { Expand = ["default_source", "invoice_settings.default_payment_method"] }); + + if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId)) + { + var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId); + + if (braintreeCustomer.DefaultPaymentMethod is PayPalAccount payPalAccount) + { + return new MaskedPayPalAccount { Email = payPalAccount.Email }; + } + + logger.LogWarning("Subscriber ({SubscriberID}) has a linked Braintree customer ({BraintreeCustomerId}) with no PayPal account.", subscriber.Id, braintreeCustomerId); + + return null; + } + + var paymentMethod = customer.InvoiceSettings.DefaultPaymentMethod != null + ? customer.InvoiceSettings.DefaultPaymentMethod.Type switch + { + "card" => MaskedPaymentMethod.From(customer.InvoiceSettings.DefaultPaymentMethod.Card), + "us_bank_account" => MaskedPaymentMethod.From(customer.InvoiceSettings.DefaultPaymentMethod.UsBankAccount), + _ => null + } + : null; + + if (paymentMethod != null) + { + return paymentMethod; + } + + if (customer.DefaultSource != null) + { + paymentMethod = customer.DefaultSource switch + { + Card card => MaskedPaymentMethod.From(card), + BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount), + Source { Card: not null } source => MaskedPaymentMethod.From(source.Card), + _ => null + }; + + if (paymentMethod != null) + { + return paymentMethod; + } + } + + var setupIntentId = await setupIntentCache.Get(subscriber.Id); + + if (string.IsNullOrEmpty(setupIntentId)) + { + return null; + } + + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }); + + // ReSharper disable once ConvertIfStatementToReturnStatement + if (!setupIntent.IsUnverifiedBankAccount()) + { + return null; + } + + return MaskedPaymentMethod.From(setupIntent); + } +} diff --git a/src/Core/Billing/Payment/Registrations.cs b/src/Core/Billing/Payment/Registrations.cs new file mode 100644 index 0000000000..1cc7914f10 --- /dev/null +++ b/src/Core/Billing/Payment/Registrations.cs @@ -0,0 +1,24 @@ +using Bit.Core.Billing.Payment.Clients; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Queries; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Billing.Payment; + +public static class Registrations +{ + public static void AddPaymentOperations(this IServiceCollection services) + { + // Commands + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Queries + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } +} diff --git a/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs b/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs index ef8d33304e..05beccdb60 100644 --- a/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs +++ b/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs @@ -6,7 +6,7 @@ namespace Bit.Core.Billing.Pricing.JSON; #nullable enable -public abstract class TypeReadingJsonConverter : JsonConverter +public abstract class TypeReadingJsonConverter : JsonConverter where T : class { protected virtual string TypePropertyName => nameof(ScalableDTO.Type).ToLower(); @@ -14,7 +14,9 @@ public abstract class TypeReadingJsonConverter : JsonConverter { while (reader.Read()) { - if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString()?.ToLower() != TypePropertyName) + if (reader.CurrentDepth != 1 || + reader.TokenType != JsonTokenType.PropertyName || + reader.GetString()?.ToLower() != TypePropertyName) { continue; } @@ -25,4 +27,10 @@ public abstract class TypeReadingJsonConverter : JsonConverter return null; } + + protected T? Skip(ref Utf8JsonReader reader) + { + reader.Skip(); + return null; + } } diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs index c777d0c0d1..86f233232f 100644 --- a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs +++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs @@ -1,8 +1,8 @@ #nullable enable +using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Services; using Bit.Core.Services; @@ -20,111 +20,95 @@ public class PreviewTaxAmountCommand( ILogger logger, IPricingClient pricingClient, IStripeAdapter stripeAdapter, - ITaxService taxService) : IPreviewTaxAmountCommand + ITaxService taxService) : BillingCommand(logger), IPreviewTaxAmountCommand { - public async Task> Run(OrganizationTrialParameters parameters) - { - var (planType, productType, taxInformation) = parameters; - - var plan = await pricingClient.GetPlanOrThrow(planType); - - var options = new InvoiceCreatePreviewOptions + public Task> Run(OrganizationTrialParameters parameters) + => HandleAsync(async () => { - Currency = "usd", - CustomerDetails = new InvoiceCustomerDetailsOptions + var (planType, productType, taxInformation) = parameters; + + var plan = await pricingClient.GetPlanOrThrow(planType); + + var options = new InvoiceCreatePreviewOptions { - Address = new AddressOptions + Currency = "usd", + CustomerDetails = new InvoiceCustomerDetailsOptions { - Country = taxInformation.Country, - PostalCode = taxInformation.PostalCode - } - }, - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = [ - new InvoiceSubscriptionDetailsItemOptions + Address = new AddressOptions { - Price = plan.HasNonSeatBasedPasswordManagerPlan() ? plan.PasswordManager.StripePlanId : plan.PasswordManager.StripeSeatPlanId, - Quantity = 1 + Country = taxInformation.Country, + PostalCode = taxInformation.PostalCode } - ] - } - }; - - if (productType == ProductType.SecretsManager) - { - options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions - { - Price = plan.SecretsManager.StripeSeatPlanId, - Quantity = 1 - }); - - options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone; - } - - if (!string.IsNullOrEmpty(taxInformation.TaxId)) - { - var taxIdType = taxService.GetStripeTaxCode( - taxInformation.Country, - taxInformation.TaxId); - - if (string.IsNullOrEmpty(taxIdType)) - { - return BadRequest.UnknownTaxIdType; - } - - options.CustomerDetails.TaxIds = [ - new InvoiceCustomerDetailsTaxIdOptions + }, + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { - Type = taxIdType, - Value = taxInformation.TaxId + Items = + [ + new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.HasNonSeatBasedPasswordManagerPlan() + ? plan.PasswordManager.StripePlanId + : plan.PasswordManager.StripeSeatPlanId, + Quantity = 1 + } + ] } - ]; - - if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) - { - options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions - { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{parameters.TaxInformation.TaxId}" - }); - } - } - - if (planType.GetProductTier() == ProductTierType.Families) - { - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - } - else - { - options.AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = options.CustomerDetails.Address.Country == "US" || - options.CustomerDetails.TaxIds is [_, ..] }; - } - try - { + if (productType == ProductType.SecretsManager) + { + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeSeatPlanId, + Quantity = 1 + }); + + options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone; + } + + if (!string.IsNullOrEmpty(taxInformation.TaxId)) + { + var taxIdType = taxService.GetStripeTaxCode( + taxInformation.Country, + taxInformation.TaxId); + + if (string.IsNullOrEmpty(taxIdType)) + { + return new BadRequest( + "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance."); + } + + options.CustomerDetails.TaxIds = + [ + new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = taxInformation.TaxId } + ]; + + if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) + { + options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions + { + Type = StripeConstants.TaxIdType.EUVAT, + Value = $"ES{parameters.TaxInformation.TaxId}" + }); + } + } + + if (planType.GetProductTier() == ProductTierType.Families) + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + } + else + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = options.CustomerDetails.Address.Country == "US" || + options.CustomerDetails.TaxIds is [_, ..] + }; + } + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); return Convert.ToDecimal(invoice.Tax) / 100; - } - catch (StripeException stripeException) when (stripeException.StripeError.Code == - StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) - { - return BadRequest.TaxLocationInvalid; - } - catch (StripeException stripeException) when (stripeException.StripeError.Code == - StripeConstants.ErrorCodes.TaxIdInvalid) - { - return BadRequest.TaxIdNumberInvalid; - } - catch (StripeException stripeException) - { - logger.LogError(stripeException, "Stripe responded with an error during {Operation}. Code: {Code}", nameof(PreviewTaxAmountCommand), stripeException.StripeError.Code); - return new Unhandled(); - } - } + }); } #region Command Parameters diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2a3b619de6..5c0bd3216e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -155,6 +155,7 @@ public static class FeatureFlagKeys public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; + public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/test/Api.Test/Billing/Attributes/InjectOrganizationAttributeTests.cs b/test/Api.Test/Billing/Attributes/InjectOrganizationAttributeTests.cs new file mode 100644 index 0000000000..252c457924 --- /dev/null +++ b/test/Api.Test/Billing/Attributes/InjectOrganizationAttributeTests.cs @@ -0,0 +1,132 @@ +using Bit.Api.Billing.Attributes; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.Api; +using Bit.Core.Repositories; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Billing.Attributes; + +public class InjectOrganizationAttributeTests +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly ActionExecutionDelegate _next; + private readonly ActionExecutingContext _context; + private readonly Organization _organization; + private readonly Guid _organizationId; + + public InjectOrganizationAttributeTests() + { + _organizationRepository = Substitute.For(); + _organizationId = Guid.NewGuid(); + _organization = new Organization { Id = _organizationId }; + + var httpContext = new DefaultHttpContext(); + var services = new ServiceCollection(); + services.AddScoped(_ => _organizationRepository); + httpContext.RequestServices = services.BuildServiceProvider(); + + var routeData = new RouteData { Values = { ["organizationId"] = _organizationId.ToString() } }; + + var actionContext = new ActionContext( + httpContext, + routeData, + new ActionDescriptor(), + new ModelStateDictionary() + ); + + _next = () => Task.FromResult(new ActionExecutedContext( + actionContext, + new List(), + new object())); + + _context = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + new object()); + } + + [Fact] + public async Task OnActionExecutionAsync_WithExistingOrganization_InjectsOrganization() + { + var attribute = new InjectOrganizationAttribute(); + _organizationRepository.GetByIdAsync(_organizationId) + .Returns(_organization); + + var parameter = new ParameterDescriptor + { + Name = "organization", + ParameterType = typeof(Organization) + }; + _context.ActionDescriptor.Parameters = [parameter]; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Equal(_organization, _context.ActionArguments["organization"]); + } + + [Fact] + public async Task OnActionExecutionAsync_WithNonExistentOrganization_ReturnsNotFound() + { + var attribute = new InjectOrganizationAttribute(); + _organizationRepository.GetByIdAsync(_organizationId) + .Returns((Organization)null); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (NotFoundObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Organization not found.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithInvalidOrganizationId_ReturnsBadRequest() + { + var attribute = new InjectOrganizationAttribute(); + _context.RouteData.Values["organizationId"] = "not-a-guid"; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (BadRequestObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithMissingOrganizationId_ReturnsBadRequest() + { + var attribute = new InjectOrganizationAttribute(); + _context.RouteData.Values.Clear(); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (BadRequestObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Route parameter 'organizationId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithoutOrganizationParameter_ContinuesExecution() + { + var attribute = new InjectOrganizationAttribute(); + _organizationRepository.GetByIdAsync(_organizationId) + .Returns(_organization); + + _context.ActionDescriptor.Parameters = Array.Empty(); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Empty(_context.ActionArguments); + } +} diff --git a/test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs b/test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs new file mode 100644 index 0000000000..0a3e19f8b1 --- /dev/null +++ b/test/Api.Test/Billing/Attributes/InjectProviderAttributeTests.cs @@ -0,0 +1,190 @@ +using Bit.Api.Billing.Attributes; +using Bit.Api.Models.Public.Response; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Billing.Attributes; + +public class InjectProviderAttributeTests +{ + private readonly IProviderRepository _providerRepository; + private readonly ICurrentContext _currentContext; + private readonly ActionExecutionDelegate _next; + private readonly ActionExecutingContext _context; + private readonly Provider _provider; + private readonly Guid _providerId; + + public InjectProviderAttributeTests() + { + _providerRepository = Substitute.For(); + _currentContext = Substitute.For(); + _providerId = Guid.NewGuid(); + _provider = new Provider { Id = _providerId }; + + var httpContext = new DefaultHttpContext(); + var services = new ServiceCollection(); + services.AddScoped(_ => _providerRepository); + services.AddScoped(_ => _currentContext); + httpContext.RequestServices = services.BuildServiceProvider(); + + var routeData = new RouteData { Values = { ["providerId"] = _providerId.ToString() } }; + + var actionContext = new ActionContext( + httpContext, + routeData, + new ActionDescriptor(), + new ModelStateDictionary() + ); + + _next = () => Task.FromResult(new ActionExecutedContext( + actionContext, + new List(), + new object())); + + _context = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + new object()); + } + + [Fact] + public async Task OnActionExecutionAsync_WithExistingProvider_InjectsProvider() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderProviderAdmin(_providerId).Returns(true); + + var parameter = new ParameterDescriptor + { + Name = "provider", + ParameterType = typeof(Provider) + }; + _context.ActionDescriptor.Parameters = [parameter]; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Equal(_provider, _context.ActionArguments["provider"]); + } + + [Fact] + public async Task OnActionExecutionAsync_WithNonExistentProvider_ReturnsNotFound() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _providerRepository.GetByIdAsync(_providerId).Returns((Provider)null); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (NotFoundObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Provider not found.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithInvalidProviderId_ReturnsBadRequest() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _context.RouteData.Values["providerId"] = "not-a-guid"; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (BadRequestObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithMissingProviderId_ReturnsBadRequest() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _context.RouteData.Values.Clear(); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (BadRequestObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Route parameter 'providerId' is missing or invalid.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithoutProviderParameter_ContinuesExecution() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderProviderAdmin(_providerId).Returns(true); + + _context.ActionDescriptor.Parameters = Array.Empty(); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Empty(_context.ActionArguments); + } + + [Fact] + public async Task OnActionExecutionAsync_UnauthorizedProviderAdmin_ReturnsUnauthorized() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderProviderAdmin(_providerId).Returns(false); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (UnauthorizedObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_UnauthorizedServiceUser_ReturnsUnauthorized() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderUser(_providerId).Returns(false); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (UnauthorizedObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_AuthorizedProviderAdmin_Succeeds() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ProviderAdmin); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderProviderAdmin(_providerId).Returns(true); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Null(_context.Result); + } + + [Fact] + public async Task OnActionExecutionAsync_AuthorizedServiceUser_Succeeds() + { + var attribute = new InjectProviderAttribute(ProviderUserType.ServiceUser); + _providerRepository.GetByIdAsync(_providerId).Returns(_provider); + _currentContext.ProviderUser(_providerId).Returns(true); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Null(_context.Result); + } +} diff --git a/test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs b/test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs new file mode 100644 index 0000000000..5c26cca64a --- /dev/null +++ b/test/Api.Test/Billing/Attributes/InjectUserAttributesTests.cs @@ -0,0 +1,129 @@ +using System.Security.Claims; +using Bit.Api.Billing.Attributes; +using Bit.Core.Entities; +using Bit.Core.Models.Api; +using Bit.Core.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Billing.Attributes; + +public class InjectUserAttributesTests +{ + private readonly IUserService _userService; + private readonly ActionExecutionDelegate _next; + private readonly ActionExecutingContext _context; + private readonly User _user; + + public InjectUserAttributesTests() + { + _userService = Substitute.For(); + _user = new User { Id = Guid.NewGuid() }; + + var httpContext = new DefaultHttpContext(); + var services = new ServiceCollection(); + services.AddScoped(_ => _userService); + httpContext.RequestServices = services.BuildServiceProvider(); + + var actionContext = new ActionContext( + httpContext, + new RouteData(), + new ActionDescriptor(), + new ModelStateDictionary() + ); + + _next = () => Task.FromResult(new ActionExecutedContext( + actionContext, + new List(), + new object())); + + _context = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + new object()); + } + + [Fact] + public async Task OnActionExecutionAsync_WithAuthorizedUser_InjectsUser() + { + var attribute = new InjectUserAttribute(); + _userService.GetUserByPrincipalAsync(Arg.Any()) + .Returns(_user); + + var parameter = new ParameterDescriptor + { + Name = "user", + ParameterType = typeof(User) + }; + _context.ActionDescriptor.Parameters = [parameter]; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Equal(_user, _context.ActionArguments["user"]); + } + + [Fact] + public async Task OnActionExecutionAsync_WithUnauthorizedUser_ReturnsUnauthorized() + { + var attribute = new InjectUserAttribute(); + _userService.GetUserByPrincipalAsync(Arg.Any()) + .Returns((User)null); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.IsType(_context.Result); + var result = (UnauthorizedObjectResult)_context.Result; + Assert.IsType(result.Value); + Assert.Equal("Unauthorized.", ((ErrorResponseModel)result.Value).Message); + } + + [Fact] + public async Task OnActionExecutionAsync_WithoutUserParameter_ContinuesExecution() + { + var attribute = new InjectUserAttribute(); + _userService.GetUserByPrincipalAsync(Arg.Any()) + .Returns(_user); + + _context.ActionDescriptor.Parameters = Array.Empty(); + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Empty(_context.ActionArguments); + } + + [Fact] + public async Task OnActionExecutionAsync_WithMultipleParameters_InjectsUserCorrectly() + { + var attribute = new InjectUserAttribute(); + _userService.GetUserByPrincipalAsync(Arg.Any()) + .Returns(_user); + + var parameters = new[] + { + new ParameterDescriptor + { + Name = "otherParam", + ParameterType = typeof(string) + }, + new ParameterDescriptor + { + Name = "user", + ParameterType = typeof(User) + } + }; + _context.ActionDescriptor.Parameters = parameters; + + await attribute.OnActionExecutionAsync(_context, _next); + + Assert.Single(_context.ActionArguments); + Assert.Equal(_user, _context.ActionArguments["user"]); + } +} diff --git a/test/Core.Test/Billing/Extensions/StripeExtensions.cs b/test/Core.Test/Billing/Extensions/StripeExtensions.cs new file mode 100644 index 0000000000..44948bbfed --- /dev/null +++ b/test/Core.Test/Billing/Extensions/StripeExtensions.cs @@ -0,0 +1,18 @@ +using Bit.Core.Billing.Payment.Models; +using Stripe; + +namespace Bit.Core.Test.Billing.Extensions; + +public static class StripeExtensions +{ + public static bool HasExpansions(this BaseOptions options, params string[] expansions) + => expansions.All(expansion => options.Expand.Contains(expansion)); + + public static bool Matches(this AddressOptions address, BillingAddress billingAddress) => + address.Country == billingAddress.Country && + address.PostalCode == billingAddress.PostalCode && + address.Line1 == billingAddress.Line1 && + address.Line2 == billingAddress.Line2 && + address.City == billingAddress.City && + address.State == billingAddress.State; +} diff --git a/test/Core.Test/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommandTests.cs new file mode 100644 index 0000000000..800c3ec3ae --- /dev/null +++ b/test/Core.Test/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommandTests.cs @@ -0,0 +1,94 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Payment.Clients; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; +using Invoice = BitPayLight.Models.Invoice.Invoice; + +namespace Bit.Core.Test.Billing.Payment.Commands; + +public class CreateBitPayInvoiceForCreditCommandTests +{ + private readonly IBitPayClient _bitPayClient = Substitute.For(); + private readonly GlobalSettings _globalSettings = new() + { + BitPay = new GlobalSettings.BitPaySettings { NotificationUrl = "https://example.com/bitpay/notification" } + }; + private const string _redirectUrl = "https://bitwarden.com/redirect"; + private readonly CreateBitPayInvoiceForCreditCommand _command; + + public CreateBitPayInvoiceForCreditCommandTests() + { + _command = new CreateBitPayInvoiceForCreditCommand( + _bitPayClient, + _globalSettings, + Substitute.For>()); + } + + [Fact] + public async Task Run_User_CreatesInvoice_ReturnsInvoiceUrl() + { + var user = new User { Id = Guid.NewGuid(), Email = "user@gmail.com" }; + + _bitPayClient.CreateInvoice(Arg.Is(options => + options.Buyer.Email == user.Email && + options.Buyer.Name == user.Email && + options.NotificationUrl == _globalSettings.BitPay.NotificationUrl && + options.PosData == $"userId:{user.Id},accountCredit:1" && + // ReSharper disable once CompareOfFloatsByEqualityOperator + options.Price == Convert.ToDouble(10M) && + options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" }); + + var result = await _command.Run(user, 10M, _redirectUrl); + + Assert.True(result.IsT0); + var invoiceUrl = result.AsT0; + Assert.Equal("https://bitpay.com/invoice/123", invoiceUrl); + } + + [Fact] + public async Task Run_Organization_CreatesInvoice_ReturnsInvoiceUrl() + { + var organization = new Organization { Id = Guid.NewGuid(), BillingEmail = "organization@example.com", Name = "Organization" }; + + _bitPayClient.CreateInvoice(Arg.Is(options => + options.Buyer.Email == organization.BillingEmail && + options.Buyer.Name == organization.Name && + options.NotificationUrl == _globalSettings.BitPay.NotificationUrl && + options.PosData == $"organizationId:{organization.Id},accountCredit:1" && + // ReSharper disable once CompareOfFloatsByEqualityOperator + options.Price == Convert.ToDouble(10M) && + options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" }); + + var result = await _command.Run(organization, 10M, _redirectUrl); + + Assert.True(result.IsT0); + var invoiceUrl = result.AsT0; + Assert.Equal("https://bitpay.com/invoice/123", invoiceUrl); + } + + [Fact] + public async Task Run_Provider_CreatesInvoice_ReturnsInvoiceUrl() + { + var provider = new Provider { Id = Guid.NewGuid(), BillingEmail = "organization@example.com", Name = "Provider" }; + + _bitPayClient.CreateInvoice(Arg.Is(options => + options.Buyer.Email == provider.BillingEmail && + options.Buyer.Name == provider.Name && + options.NotificationUrl == _globalSettings.BitPay.NotificationUrl && + options.PosData == $"providerId:{provider.Id},accountCredit:1" && + // ReSharper disable once CompareOfFloatsByEqualityOperator + options.Price == Convert.ToDouble(10M) && + options.RedirectUrl == _redirectUrl)).Returns(new Invoice { Url = "https://bitpay.com/invoice/123" }); + + var result = await _command.Run(provider, 10M, _redirectUrl); + + Assert.True(result.IsT0); + var invoiceUrl = result.AsT0; + Assert.Equal("https://bitpay.com/invoice/123", invoiceUrl); + } +} diff --git a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs new file mode 100644 index 0000000000..453d0c78e9 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs @@ -0,0 +1,349 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Services; +using Bit.Core.Test.Billing.Extensions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Commands; + +using static StripeConstants; + +public class UpdateBillingAddressCommandTests +{ + private readonly IStripeAdapter _stripeAdapter; + private readonly UpdateBillingAddressCommand _command; + + public UpdateBillingAddressCommandTests() + { + _stripeAdapter = Substitute.For(); + _command = new UpdateBillingAddressCommand( + Substitute.For>(), + _stripeAdapter); + } + + [Fact] + public async Task Run_PersonalOrganization_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.FamiliesAnnually, + GatewayCustomerId = "cus_123", + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions") + )).Returns(customer); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input, output); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + + [Fact] + public async Task Run_BusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_123", + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions", "tax_ids") && + options.TaxExempt == TaxExempt.None + )).Returns(customer); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input, output); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + + [Fact] + public async Task Run_BusinessOrganization_RemovingTaxId_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_123", + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }, + Id = organization.GatewayCustomerId, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + }, + TaxIds = new StripeList + { + Data = + [ + new TaxId { Id = "tax_id_123", Type = "us_ein", Value = "123456789" } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions", "tax_ids") && + options.TaxExempt == TaxExempt.None + )).Returns(customer); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input, output); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + + await _stripeAdapter.Received(1).TaxIdDeleteAsync(customer.Id, "tax_id_123"); + } + + [Fact] + public async Task Run_NonUSBusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_123", + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "DE", + PostalCode = "10115", + Line1 = "Friedrichstraße 123", + Line2 = "Stock 3", + City = "Berlin", + State = "Berlin" + }; + + var customer = new Customer + { + Address = new Address + { + Country = "DE", + PostalCode = "10115", + Line1 = "Friedrichstraße 123", + Line2 = "Stock 3", + City = "Berlin", + State = "Berlin" + }, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions", "tax_ids") && + options.TaxExempt == TaxExempt.Reverse + )).Returns(customer); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input, output); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + + [Fact] + public async Task Run_BusinessOrganizationWithSpanishCIF_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_123", + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "ES", + PostalCode = "28001", + Line1 = "Calle de Serrano 41", + Line2 = "Planta 3", + City = "Madrid", + State = "Madrid", + TaxId = new TaxID(TaxIdType.SpanishNIF, "A12345678") + }; + + var customer = new Customer + { + Address = new Address + { + Country = "ES", + PostalCode = "28001", + Line1 = "Calle de Serrano 41", + Line2 = "Planta 3", + City = "Madrid", + State = "Madrid" + }, + Id = organization.GatewayCustomerId, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions", "tax_ids") && + options.TaxExempt == TaxExempt.Reverse + )).Returns(customer); + + _stripeAdapter + .TaxIdCreateAsync(customer.Id, + Arg.Is(options => options.Type == TaxIdType.EUVAT)) + .Returns(new TaxId { Type = TaxIdType.EUVAT, Value = "ESA12345678" }); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input with { TaxId = new TaxID(TaxIdType.EUVAT, "ESA12345678") }, output); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + + await _stripeAdapter.Received(1).TaxIdCreateAsync(organization.GatewayCustomerId, Arg.Is( + options => options.Type == TaxIdType.SpanishNIF && + options.Value == input.TaxId.Value)); + } +} diff --git a/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs new file mode 100644 index 0000000000..e7bc5c787c --- /dev/null +++ b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs @@ -0,0 +1,399 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Test.Billing.Extensions; +using Braintree; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; +using Address = Stripe.Address; +using Customer = Stripe.Customer; +using PaymentMethod = Stripe.PaymentMethod; + +namespace Bit.Core.Test.Billing.Payment.Commands; + +using static StripeConstants; + +public class UpdatePaymentMethodCommandTests +{ + private readonly IBraintreeGateway _braintreeGateway = Substitute.For(); + private readonly IGlobalSettings _globalSettings = Substitute.For(); + private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly UpdatePaymentMethodCommand _command; + + public UpdatePaymentMethodCommandTests() + { + _command = new UpdatePaymentMethodCommand( + _braintreeGateway, + _globalSettings, + Substitute.For>(), + _setupIntentCache, + _stripeAdapter, + _subscriberService); + } + + [Fact] + public async Task Run_BankAccount_MakesCorrectInvocations_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + const string token = "TOKEN"; + + var setupIntent = new SetupIntent + { + Id = "seti_123", + PaymentMethod = + new PaymentMethod + { + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + }, + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + Status = "requires_action" + }; + + _stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress + { + Country = "US", + PostalCode = "12345" + }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.False(maskedBankAccount.Verified); + + await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id); + } + + [Fact] + public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Id = "cus_123", + Metadata = new Dictionary + { + [MetadataKeys.BraintreeCustomerId] = "braintree_customer_id" + } + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + const string token = "TOKEN"; + + var setupIntent = new SetupIntent + { + Id = "seti_123", + PaymentMethod = + new PaymentMethod + { + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + }, + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + Status = "requires_action" + }; + + _stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress + { + Country = "US", + PostalCode = "12345" + }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.False(maskedBankAccount.Verified); + + await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id); + await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is(options => + options.Metadata[MetadataKeys.BraintreeCustomerId] == string.Empty && + options.Metadata[MetadataKeys.RetiredBraintreeCustomerId] == "braintree_customer_id")); + } + + [Fact] + public async Task Run_Card_MakesCorrectInvocations_ReturnsMaskedCard() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Id = "cus_123", + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + const string token = "TOKEN"; + + _stripeAdapter + .PaymentMethodAttachAsync(token, + Arg.Is(options => options.Customer == customer.Id)) + .Returns(new PaymentMethod + { + Type = "card", + Card = new PaymentMethodCard + { + Brand = "visa", + Last4 = "9999", + ExpMonth = 1, + ExpYear = 2028 + } + }); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = token }, new BillingAddress + { + Country = "US", + PostalCode = "12345" + }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT1); + var maskedCard = maskedPaymentMethod.AsT1; + Assert.Equal("visa", maskedCard.Brand); + Assert.Equal("9999", maskedCard.Last4); + Assert.Equal("01/2028", maskedCard.Expiration); + + await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, + Arg.Is(options => options.InvoiceSettings.DefaultPaymentMethod == token)); + } + + [Fact] + public async Task Run_Card_PropagateBillingAddress_MakesCorrectInvocations_ReturnsMaskedCard() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Id = "cus_123", + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + const string token = "TOKEN"; + + _stripeAdapter + .PaymentMethodAttachAsync(token, + Arg.Is(options => options.Customer == customer.Id)) + .Returns(new PaymentMethod + { + Type = "card", + Card = new PaymentMethodCard + { + Brand = "visa", + Last4 = "9999", + ExpMonth = 1, + ExpYear = 2028 + } + }); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = token }, new BillingAddress + { + Country = "US", + PostalCode = "12345" + }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT1); + var maskedCard = maskedPaymentMethod.AsT1; + Assert.Equal("visa", maskedCard.Brand); + Assert.Equal("9999", maskedCard.Last4); + Assert.Equal("01/2028", maskedCard.Expiration); + + await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, + Arg.Is(options => options.InvoiceSettings.DefaultPaymentMethod == token)); + + await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, + Arg.Is(options => options.Address.Country == "US" && options.Address.PostalCode == "12345")); + } + + [Fact] + public async Task Run_PayPal_ExistingBraintreeCustomer_MakesCorrectInvocations_ReturnsMaskedPayPalAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Id = "cus_123", + Metadata = new Dictionary + { + [MetadataKeys.BraintreeCustomerId] = "braintree_customer_id" + } + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + var customerGateway = Substitute.For(); + var braintreeCustomer = Substitute.For(); + braintreeCustomer.Id.Returns("braintree_customer_id"); + var existing = Substitute.For(); + existing.Email.Returns("user@gmail.com"); + existing.IsDefault.Returns(true); + existing.Token.Returns("EXISTING"); + braintreeCustomer.PaymentMethods.Returns([existing]); + customerGateway.FindAsync("braintree_customer_id").Returns(braintreeCustomer); + _braintreeGateway.Customer.Returns(customerGateway); + + var paymentMethodGateway = Substitute.For(); + var updated = Substitute.For(); + updated.Email.Returns("user@gmail.com"); + updated.Token.Returns("UPDATED"); + var updatedResult = Substitute.For>(); + updatedResult.Target.Returns(updated); + paymentMethodGateway.CreateAsync(Arg.Is(options => + options.CustomerId == braintreeCustomer.Id && options.PaymentMethodNonce == "TOKEN")) + .Returns(updatedResult); + _braintreeGateway.PaymentMethod.Returns(paymentMethodGateway); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "TOKEN" }, + new BillingAddress { Country = "US", PostalCode = "12345" }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT2); + var maskedPayPalAccount = maskedPaymentMethod.AsT2; + Assert.Equal("user@gmail.com", maskedPayPalAccount.Email); + + await customerGateway.Received(1).UpdateAsync(braintreeCustomer.Id, + Arg.Is(options => options.DefaultPaymentMethodToken == updated.Token)); + await paymentMethodGateway.Received(1).DeleteAsync(existing.Token); + } + + [Fact] + public async Task Run_PayPal_NewBraintreeCustomer_MakesCorrectInvocations_ReturnsMaskedPayPalAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Id = "cus_123", + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + _globalSettings.BaseServiceUri.Returns(new GlobalSettings.BaseServiceUriSettings(new GlobalSettings()) + { + CloudRegion = "US" + }); + + var customerGateway = Substitute.For(); + var braintreeCustomer = Substitute.For(); + braintreeCustomer.Id.Returns("braintree_customer_id"); + var payPalAccount = Substitute.For(); + payPalAccount.Email.Returns("user@gmail.com"); + payPalAccount.IsDefault.Returns(true); + payPalAccount.Token.Returns("NONCE"); + braintreeCustomer.PaymentMethods.Returns([payPalAccount]); + var createResult = Substitute.For>(); + createResult.Target.Returns(braintreeCustomer); + customerGateway.CreateAsync(Arg.Is(options => + options.Id.StartsWith(organization.BraintreeCustomerIdPrefix() + organization.Id.ToString("N").ToLower()) && + options.CustomFields[organization.BraintreeIdField()] == organization.Id.ToString() && + options.CustomFields[organization.BraintreeCloudRegionField()] == "US" && + options.Email == organization.BillingEmailAddress() && + options.PaymentMethodNonce == "TOKEN")).Returns(createResult); + _braintreeGateway.Customer.Returns(customerGateway); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "TOKEN" }, + new BillingAddress { Country = "US", PostalCode = "12345" }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT2); + var maskedPayPalAccount = maskedPaymentMethod.AsT2; + Assert.Equal("user@gmail.com", maskedPayPalAccount.Email); + + await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, + Arg.Is(options => + options.Metadata[MetadataKeys.BraintreeCustomerId] == "braintree_customer_id")); + } +} diff --git a/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs new file mode 100644 index 0000000000..4be5539cc8 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs @@ -0,0 +1,81 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Payment.Commands; +using Bit.Core.Services; +using Bit.Core.Test.Billing.Extensions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Commands; + +public class VerifyBankAccountCommandTests +{ + private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly VerifyBankAccountCommand _command; + + public VerifyBankAccountCommandTests() + { + _command = new VerifyBankAccountCommand( + Substitute.For>(), + _setupIntentCache, + _stripeAdapter); + } + + [Fact] + public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" + }; + + const string setupIntentId = "seti_123"; + + _setupIntentCache.Get(organization.Id).Returns(setupIntentId); + + var setupIntent = new SetupIntent + { + Id = setupIntentId, + PaymentMethodId = "pm_123", + PaymentMethod = + new PaymentMethod + { + Id = "pm_123", + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + }, + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + Status = "requires_action" + }; + + _stripeAdapter.SetupIntentGet(setupIntentId, + Arg.Is(options => options.HasExpansions("payment_method"))).Returns(setupIntent); + + _stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, + Arg.Is(options => options.Customer == organization.GatewayCustomerId)) + .Returns(setupIntent.PaymentMethod); + + var result = await _command.Run(organization, "DESCRIPTOR_CODE"); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.True(maskedBankAccount.Verified); + + await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id, + Arg.Is(options => options.DescriptorCode == "DESCRIPTOR_CODE")); + + await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is( + options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId)); + } +} diff --git a/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs new file mode 100644 index 0000000000..345f2dfab8 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs @@ -0,0 +1,63 @@ +using System.Text.Json; +using Bit.Core.Billing.Payment.Models; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Models; + +public class MaskedPaymentMethodTests +{ + [Fact] + public void Write_Read_BankAccount_Succeeds() + { + MaskedPaymentMethod input = new MaskedBankAccount + { + BankName = "Chase", + Last4 = "9999", + Verified = true + }; + + var json = JsonSerializer.Serialize(input); + + var output = JsonSerializer.Deserialize(json); + Assert.NotNull(output); + Assert.True(output.IsT0); + + Assert.Equivalent(input.AsT0, output.AsT0); + } + + [Fact] + public void Write_Read_Card_Succeeds() + { + MaskedPaymentMethod input = new MaskedCard + { + Brand = "visa", + Last4 = "9999", + Expiration = "01/2028" + }; + + var json = JsonSerializer.Serialize(input); + + var output = JsonSerializer.Deserialize(json); + Assert.NotNull(output); + Assert.True(output.IsT1); + + Assert.Equivalent(input.AsT1, output.AsT1); + } + + [Fact] + public void Write_Read_PayPal_Succeeds() + { + MaskedPaymentMethod input = new MaskedPayPalAccount + { + Email = "paypal-user@gmail.com" + }; + + var json = JsonSerializer.Serialize(input); + + var output = JsonSerializer.Deserialize(json); + Assert.NotNull(output); + Assert.True(output.IsT2); + + Assert.Equivalent(input.AsT2, output.AsT2); + } +} diff --git a/test/Core.Test/Billing/Payment/Queries/GetBillingAddressQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/GetBillingAddressQueryTests.cs new file mode 100644 index 0000000000..048c143a0e --- /dev/null +++ b/test/Core.Test/Billing/Payment/Queries/GetBillingAddressQueryTests.cs @@ -0,0 +1,204 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Test.Billing.Extensions; +using NSubstitute; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Queries; + +public class GetBillingAddressQueryTests +{ + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly GetBillingAddressQuery _query; + + public GetBillingAddressQueryTests() + { + _query = new GetBillingAddressQuery(_subscriberService); + } + + [Fact] + public async Task Run_ForUserWithNoAddress_ReturnsNull() + { + var user = new User(); + + var customer = new Customer(); + + _subscriberService.GetCustomer(user, Arg.Is( + options => options.Expand == null)).Returns(customer); + + var billingAddress = await _query.Run(user); + + Assert.Null(billingAddress); + } + + [Fact] + public async Task Run_ForUserWithAddress_ReturnsBillingAddress() + { + var user = new User(); + + var address = GetAddress(); + + var customer = new Customer + { + Address = address + }; + + _subscriberService.GetCustomer(user, Arg.Is( + options => options.Expand == null)).Returns(customer); + + var billingAddress = await _query.Run(user); + + AssertEquality(address, billingAddress); + } + + [Fact] + public async Task Run_ForPersonalOrganizationWithNoAddress_ReturnsNull() + { + var organization = new Organization + { + PlanType = PlanType.FamiliesAnnually + }; + + var customer = new Customer(); + + _subscriberService.GetCustomer(organization, Arg.Is( + options => options.Expand == null)).Returns(customer); + + var billingAddress = await _query.Run(organization); + + Assert.Null(billingAddress); + } + + [Fact] + public async Task Run_ForPersonalOrganizationWithAddress_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.FamiliesAnnually + }; + + var address = GetAddress(); + + var customer = new Customer + { + Address = address + }; + + _subscriberService.GetCustomer(organization, Arg.Is( + options => options.Expand == null)).Returns(customer); + + var billingAddress = await _query.Run(organization); + + AssertEquality(customer.Address, billingAddress); + } + + [Fact] + public async Task Run_ForBusinessOrganizationWithNoAddress_ReturnsNull() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually + }; + + var customer = new Customer(); + + _subscriberService.GetCustomer(organization, Arg.Is( + options => options.HasExpansions("tax_ids"))).Returns(customer); + + var billingAddress = await _query.Run(organization); + + Assert.Null(billingAddress); + } + + [Fact] + public async Task Run_ForBusinessOrganizationWithAddressAndTaxId_ReturnsBillingAddressWithTaxId() + { + var organization = new Organization + { + PlanType = PlanType.EnterpriseAnnually + }; + + var address = GetAddress(); + + var taxId = GetTaxId(); + + var customer = new Customer + { + Address = address, + TaxIds = new StripeList + { + Data = [taxId] + } + }; + + _subscriberService.GetCustomer(organization, Arg.Is( + options => options.HasExpansions("tax_ids"))).Returns(customer); + + var billingAddress = await _query.Run(organization); + + AssertEquality(address, taxId, billingAddress); + } + + [Fact] + public async Task Run_ForProviderWithAddressAndTaxId_ReturnsBillingAddressWithTaxId() + { + var provider = new Provider(); + + var address = GetAddress(); + + var taxId = GetTaxId(); + + var customer = new Customer + { + Address = address, + TaxIds = new StripeList + { + Data = [taxId] + } + }; + + _subscriberService.GetCustomer(provider, Arg.Is( + options => options.HasExpansions("tax_ids"))).Returns(customer); + + var billingAddress = await _query.Run(provider); + + AssertEquality(address, taxId, billingAddress); + } + + private static void AssertEquality(Address address, BillingAddress? billingAddress) + { + Assert.NotNull(billingAddress); + Assert.Equal(address.Country, billingAddress.Country); + Assert.Equal(address.PostalCode, billingAddress.PostalCode); + Assert.Equal(address.Line1, billingAddress.Line1); + Assert.Equal(address.Line2, billingAddress.Line2); + Assert.Equal(address.City, billingAddress.City); + Assert.Equal(address.State, billingAddress.State); + } + + private static void AssertEquality(Address address, TaxId taxId, BillingAddress? billingAddress) + { + AssertEquality(address, billingAddress); + Assert.NotNull(billingAddress!.TaxId); + Assert.Equal(taxId.Type, billingAddress.TaxId!.Code); + Assert.Equal(taxId.Value, billingAddress.TaxId!.Value); + } + + private static Address GetAddress() => new() + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }; + + private static TaxId GetTaxId() => new() { Type = "us_ein", Value = "123456789" }; +} diff --git a/test/Core.Test/Billing/Payment/Queries/GetCreditQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/GetCreditQueryTests.cs new file mode 100644 index 0000000000..55f5e85009 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Queries/GetCreditQueryTests.cs @@ -0,0 +1,41 @@ +using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Payment.Queries; + +public class GetCreditQueryTests +{ + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly GetCreditQuery _query; + + public GetCreditQueryTests() + { + _query = new GetCreditQuery(_subscriberService); + } + + [Fact] + public async Task Run_NoCustomer_ReturnsNull() + { + _subscriberService.GetCustomer(Arg.Any()).ReturnsNull(); + + var credit = await _query.Run(Substitute.For()); + + Assert.Null(credit); + } + + [Fact] + public async Task Run_ReturnsCredit() + { + _subscriberService.GetCustomer(Arg.Any()).Returns(new Customer { Balance = -1000 }); + + var credit = await _query.Run(Substitute.For()); + + Assert.NotNull(credit); + Assert.Equal(10M, credit); + } +} diff --git a/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs new file mode 100644 index 0000000000..4d82b4b5c9 --- /dev/null +++ b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs @@ -0,0 +1,327 @@ +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 Braintree; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; +using Customer = Stripe.Customer; +using PaymentMethod = Stripe.PaymentMethod; + +namespace Bit.Core.Test.Billing.Payment.Queries; + +using static StripeConstants; + +public class GetPaymentMethodQueryTests +{ + private readonly IBraintreeGateway _braintreeGateway = Substitute.For(); + private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly GetPaymentMethodQuery _query; + + public GetPaymentMethodQueryTests() + { + _query = new GetPaymentMethodQuery( + _braintreeGateway, + Substitute.For>(), + _setupIntentCache, + _stripeAdapter, + _subscriberService); + } + + [Fact] + public async Task Run_NoPaymentMethod_ReturnsNull() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.Null(maskedPaymentMethod); + } + + [Fact] + public async Task Run_BankAccount_FromPaymentMethod_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethod = new PaymentMethod + { + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + } + }, + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.True(maskedBankAccount.Verified); + } + + [Fact] + public async Task Run_BankAccount_FromSource_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + DefaultSource = new BankAccount + { + BankName = "Chase", + Last4 = "9999", + Status = "verified" + }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.True(maskedBankAccount.Verified); + } + + [Fact] + public async Task Run_BankAccount_FromSetupIntent_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + _setupIntentCache.Get(organization.Id).Returns("seti_123"); + + _stripeAdapter + .SetupIntentGet("seti_123", + Arg.Is(options => options.HasExpansions("payment_method"))).Returns( + new SetupIntent + { + PaymentMethod = new PaymentMethod + { + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + }, + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + Status = "requires_action" + }); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.False(maskedBankAccount.Verified); + } + + [Fact] + public async Task Run_Card_FromPaymentMethod_ReturnsMaskedCard() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethod = new PaymentMethod + { + Type = "card", + Card = new PaymentMethodCard + { + Brand = "visa", + Last4 = "9999", + ExpMonth = 1, + ExpYear = 2028 + } + } + }, + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT1); + var maskedCard = maskedPaymentMethod.AsT1; + Assert.Equal("visa", maskedCard.Brand); + Assert.Equal("9999", maskedCard.Last4); + Assert.Equal("01/2028", maskedCard.Expiration); + } + + [Fact] + public async Task Run_Card_FromSource_ReturnsMaskedCard() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + DefaultSource = new Card + { + Brand = "visa", + Last4 = "9999", + ExpMonth = 1, + ExpYear = 2028 + }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT1); + var maskedCard = maskedPaymentMethod.AsT1; + Assert.Equal("visa", maskedCard.Brand); + Assert.Equal("9999", maskedCard.Last4); + Assert.Equal("01/2028", maskedCard.Expiration); + } + + [Fact] + public async Task Run_Card_FromSourceCard_ReturnsMaskedCard() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + DefaultSource = new Source + { + Card = new SourceCard + { + Brand = "Visa", + Last4 = "9999", + ExpMonth = 1, + ExpYear = 2028 + } + }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT1); + var maskedCard = maskedPaymentMethod.AsT1; + Assert.Equal("visa", maskedCard.Brand); + Assert.Equal("9999", maskedCard.Last4); + Assert.Equal("01/2028", maskedCard.Expiration); + } + + [Fact] + public async Task Run_PayPalAccount_ReturnsMaskedPayPalAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + var customer = new Customer + { + Metadata = new Dictionary + { + [MetadataKeys.BraintreeCustomerId] = "braintree_customer_id" + } + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); + + var customerGateway = Substitute.For(); + var braintreeCustomer = Substitute.For(); + var payPalAccount = Substitute.For(); + payPalAccount.Email.Returns("user@gmail.com"); + payPalAccount.IsDefault.Returns(true); + braintreeCustomer.PaymentMethods.Returns([payPalAccount]); + customerGateway.FindAsync("braintree_customer_id").Returns(braintreeCustomer); + _braintreeGateway.Customer.Returns(customerGateway); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.NotNull(maskedPaymentMethod); + Assert.True(maskedPaymentMethod.IsT2); + var maskedPayPalAccount = maskedPaymentMethod.AsT2; + Assert.Equal("user@gmail.com", maskedPayPalAccount.Email); + } +} diff --git a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs index c35dc275e6..ee5625d522 100644 --- a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs +++ b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs @@ -1,6 +1,5 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Commands; using Bit.Core.Billing.Tax.Services; @@ -8,7 +7,6 @@ using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; using NSubstitute; -using NSubstitute.ExceptionExtensions; using Stripe; using Xunit; using static Bit.Core.Billing.Tax.Commands.OrganizationTrialParameters; @@ -273,74 +271,6 @@ public class PreviewTaxAmountCommandTests // Assert Assert.True(result.IsT1); var badRequest = result.AsT1; - Assert.Equal(BillingErrorTranslationKeys.UnknownTaxIdType, badRequest.TranslationKey); - } - - [Fact] - public async Task Run_CustomerTaxLocationInvalid_BadRequest() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Throws(new StripeException - { - StripeError = new StripeError { Code = StripeConstants.ErrorCodes.CustomerTaxLocationInvalid } - }); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT1); - var badRequest = result.AsT1; - Assert.Equal(BillingErrorTranslationKeys.CustomerTaxLocationInvalid, badRequest.TranslationKey); - } - - [Fact] - public async Task Run_TaxIdInvalid_BadRequest() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Throws(new StripeException - { - StripeError = new StripeError { Code = StripeConstants.ErrorCodes.TaxIdInvalid } - }); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT1); - var badRequest = result.AsT1; - Assert.Equal(BillingErrorTranslationKeys.TaxIdInvalid, badRequest.TranslationKey); + Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response); } } From 9a973846704910657092dd449d732792ea57bf1a Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 10 Jul 2025 08:34:45 -0500 Subject: [PATCH 043/326] [PM-23575] Use the input text as question and avoid additional call to freshdesk (#6073) --- .../Controllers/FreshdeskController.cs | 47 +----------- .../Models/FreshdeskViewTicketModel.cs | 47 ------------ src/Billing/Models/FreshdeskWebhookModel.cs | 6 ++ .../Controllers/FreshdeskControllerTests.cs | 75 ++----------------- 4 files changed, 13 insertions(+), 162 deletions(-) delete mode 100644 src/Billing/Models/FreshdeskViewTicketModel.cs diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 40c9c39d95..3b7415121e 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -144,7 +144,7 @@ public class FreshdeskController : Controller [HttpPost("webhook-onyx-ai")] public async Task PostWebhookOnyxAi([FromQuery, Required] string key, - [FromBody, Required] FreshdeskWebhookModel model) + [FromBody, Required] FreshdeskOnyxAiWebhookModel model) { // ensure that the key is from Freshdesk if (!IsValidRequestFromFreshdesk(key)) @@ -152,28 +152,8 @@ public class FreshdeskController : Controller return new BadRequestResult(); } - // get ticket info from Freshdesk - var getTicketRequest = new HttpRequestMessage(HttpMethod.Get, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", model.TicketId)); - var getTicketResponse = await CallFreshdeskApiAsync(getTicketRequest); - - // check if we have a valid response from freshdesk - if (getTicketResponse.StatusCode != System.Net.HttpStatusCode.OK) - { - _logger.LogError("Error getting ticket info from Freshdesk. Ticket Id: {0}. Status code: {1}", - model.TicketId, getTicketResponse.StatusCode); - return BadRequest("Failed to retrieve ticket info from Freshdesk"); - } - - // extract info from the response - var ticketInfo = await ExtractTicketInfoFromResponse(getTicketResponse); - if (ticketInfo == null) - { - return BadRequest("Failed to extract ticket info from Freshdesk response"); - } - // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(ticketInfo.DescriptionText); + var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText); var onyxRequest = new HttpRequestMessage(HttpMethod.Post, string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl)) { @@ -249,29 +229,6 @@ public class FreshdeskController : Controller } } - private async Task ExtractTicketInfoFromResponse(HttpResponseMessage getTicketResponse) - { - var responseString = string.Empty; - try - { - responseString = await getTicketResponse.Content.ReadAsStringAsync(); - var ticketInfo = JsonSerializer.Deserialize(responseString, - options: new System.Text.Json.JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }); - - return ticketInfo; - } - catch (System.Exception ex) - { - _logger.LogError("Error deserializing ticket info from Freshdesk response. Response: {0}. Exception {1}", - responseString, ex.ToString()); - } - - return null; - } - private async Task CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) { try diff --git a/src/Billing/Models/FreshdeskViewTicketModel.cs b/src/Billing/Models/FreshdeskViewTicketModel.cs deleted file mode 100644 index e4d485072c..0000000000 --- a/src/Billing/Models/FreshdeskViewTicketModel.cs +++ /dev/null @@ -1,47 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -namespace Bit.Billing.Models; - -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -public class FreshdeskViewTicketModel -{ - [JsonPropertyName("spam")] - public bool? Spam { get; set; } - - [JsonPropertyName("priority")] - public int? Priority { get; set; } - - [JsonPropertyName("source")] - public int? Source { get; set; } - - [JsonPropertyName("status")] - public int? Status { get; set; } - - [JsonPropertyName("subject")] - public string Subject { get; set; } - - [JsonPropertyName("support_email")] - public string SupportEmail { get; set; } - - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("description")] - public string Description { get; set; } - - [JsonPropertyName("description_text")] - public string DescriptionText { get; set; } - - [JsonPropertyName("created_at")] - public DateTime CreatedAt { get; set; } - - [JsonPropertyName("updated_at")] - public DateTime UpdatedAt { get; set; } - - [JsonPropertyName("tags")] - public List Tags { get; set; } -} diff --git a/src/Billing/Models/FreshdeskWebhookModel.cs b/src/Billing/Models/FreshdeskWebhookModel.cs index 19c94d5eba..aac0e9339d 100644 --- a/src/Billing/Models/FreshdeskWebhookModel.cs +++ b/src/Billing/Models/FreshdeskWebhookModel.cs @@ -16,3 +16,9 @@ public class FreshdeskWebhookModel [JsonPropertyName("ticket_tags")] public string TicketTags { get; set; } } + +public class FreshdeskOnyxAiWebhookModel : FreshdeskWebhookModel +{ + [JsonPropertyName("ticket_description_text")] + public string TicketDescriptionText { get; set; } +} diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 90f8a09ea0..f0a34ff232 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -112,7 +112,7 @@ public class FreshdeskControllerTests [BitAutoData((string)null)] [BitAutoData(WebhookKey, null)] public async Task PostWebhookOnyxAi_InvalidWebhookKey_results_in_BadRequest( - string freshdeskWebhookKey, FreshdeskWebhookModel model, + string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, BillingSettings billingSettings, SutProvider sutProvider) { sutProvider.GetDependency>() @@ -124,59 +124,11 @@ public class FreshdeskControllerTests Assert.Equal(StatusCodes.Status400BadRequest, statusCodeResult.StatusCode); } - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_invalid_ticketid_results_in_BadRequest( - string freshdeskWebhookKey, FreshdeskWebhookModel model, SutProvider sutProvider) - { - sutProvider.GetDependency>() - .Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); - - var mockHttpMessageHandler = Substitute.ForPartsOf(); - var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); - mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockResponse); - var httpClient = new HttpClient(mockHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var result = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); - } - - [Theory] - [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_invalid_freshdesk_response_results_in_BadRequest( - string freshdeskWebhookKey, FreshdeskWebhookModel model, - SutProvider sutProvider) - { - sutProvider.GetDependency>() - .Value.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); - - var mockHttpMessageHandler = Substitute.ForPartsOf(); - var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("non json content. expect json deserializer to throw error") - }; - mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockResponse); - var httpClient = new HttpClient(mockHttpMessageHandler); - - sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); - - var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - - var result = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); - } - [Theory] [BitAutoData(WebhookKey)] public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest( - string freshdeskWebhookKey, FreshdeskWebhookModel model, - FreshdeskViewTicketModel freshdeskTicketInfo, SutProvider sutProvider) + string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, + SutProvider sutProvider) { var billingSettings = sutProvider.GetDependency>().Value; billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); @@ -184,12 +136,6 @@ public class FreshdeskControllerTests // mocking freshdesk Api request for ticket info var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); - var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo)) - }; - mockFreshdeskHttpMessageHandler.Send(Arg.Any(), Arg.Any()) - .Returns(mockFreshdeskResponse); var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); // mocking Onyx api response given a ticket description @@ -211,8 +157,7 @@ public class FreshdeskControllerTests [Theory] [BitAutoData(WebhookKey)] public async Task PostWebhookOnyxAi_success( - string freshdeskWebhookKey, FreshdeskWebhookModel model, - FreshdeskViewTicketModel freshdeskTicketInfo, + string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, OnyxAnswerWithCitationResponseModel onyxResponse, SutProvider sutProvider) { @@ -220,18 +165,8 @@ public class FreshdeskControllerTests billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); - // mocking freshdesk Api request for ticket info (GET) - var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); - var mockFreshdeskResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(JsonSerializer.Serialize(freshdeskTicketInfo)) - }; - mockFreshdeskHttpMessageHandler.Send( - Arg.Is(_ => _.Method == HttpMethod.Get), - Arg.Any()) - .Returns(mockFreshdeskResponse); - // mocking freshdesk api add note request (POST) + var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); var mockFreshdeskAddNoteResponse = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest); mockFreshdeskHttpMessageHandler.Send( Arg.Is(_ => _.Method == HttpMethod.Post), From df004d0af0216777600e857338dd2bcd08322f18 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Thu, 10 Jul 2025 10:03:55 -0500 Subject: [PATCH 044/326] PM-21685 fixing flaky test (#6065) * PM-21685 fixing flaky test * PM-21685 adding a comment to explain why imports changed for test --- .../ImportCiphersControllerTests.cs | 85 ++++++++++++++----- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs index 457b9fd47d..53d9d2a1f8 100644 --- a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -20,6 +20,7 @@ using NSubstitute; using NSubstitute.ClearExtensions; using Xunit; using GlobalSettings = Bit.Core.Settings.GlobalSettings; +using ImportCiphersLimitationSettings = Bit.Core.Settings.GlobalSettings.ImportCiphersLimitationSettings; namespace Bit.Api.Test.Tools.Controllers; @@ -27,6 +28,12 @@ namespace Bit.Api.Test.Tools.Controllers; [SutProviderCustomize] public class ImportCiphersControllerTests { + private readonly ImportCiphersLimitationSettings _organizationCiphersLimitations = new() + { + CiphersLimit = 40000, + CollectionRelationshipsLimit = 80000, + CollectionsLimit = 2000 + }; /************************* * PostImport - Individual @@ -35,7 +42,7 @@ public class ImportCiphersControllerTests public async Task PostImportIndividual_ImportCiphersRequestModel_BadRequestException(SutProvider sutProvider, IFixture fixture) { // Arrange - sutProvider.GetDependency() + sutProvider.GetDependency() .SelfHosted = false; var ciphers = fixture.CreateMany(7001).ToArray(); var model = new ImportCiphersRequestModel @@ -90,24 +97,27 @@ public class ImportCiphersControllerTests ****************************/ [Theory, BitAutoData] - public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException(SutProvider sutProvider, IFixture fixture) + public async Task PostImportOrganization_ImportOrganizationCiphersRequestModel_BadRequestException( + SutProvider sutProvider, + IFixture fixture) { // Arrange - var globalSettings = sutProvider.GetDependency(); - globalSettings.SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + // Limits are set in appsettings.json, making values small for test to run faster. + sutProvider.GetDependency() + .ImportCiphersLimitation = new() + { + CiphersLimit = 4, + CollectionRelationshipsLimit = 8, + CollectionsLimit = 2 + }; var userService = sutProvider.GetDependency(); userService.GetProperUserId(Arg.Any()) .Returns(null as Guid?); - globalSettings.ImportCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings() - { // limits are set in appsettings.json, making values small for test to run faster. - CiphersLimit = 200, - CollectionsLimit = 400, - CollectionRelationshipsLimit = 20 - }; - - var ciphers = fixture.CreateMany(201).ToArray(); + var ciphers = fixture.CreateMany(5).ToArray(); var model = new ImportOrganizationCiphersRequestModel { Collections = null, @@ -133,7 +143,10 @@ public class ImportCiphersControllerTests var orgIdGuid = Guid.Parse(orgId); var existingCollections = fixture.CreateMany(2).ToArray(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; sutProvider.GetDependency() .GetProperUserId(Arg.Any()) @@ -196,7 +209,15 @@ public class ImportCiphersControllerTests var orgIdGuid = Guid.Parse(orgId); var existingCollections = fixture.CreateMany(2).ToArray(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; + + var importCiphersLimitation = new GlobalSettings.ImportCiphersLimitationSettings(); + importCiphersLimitation.CiphersLimit = 40000; + importCiphersLimitation.CollectionRelationshipsLimit = 80000; + importCiphersLimitation.CollectionsLimit = 2000; sutProvider.GetDependency() .GetProperUserId(Arg.Any()) @@ -259,7 +280,10 @@ public class ImportCiphersControllerTests var orgIdGuid = Guid.Parse(orgId); var existingCollections = fixture.CreateMany(2).ToArray(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; SetupUserService(sutProvider, user); @@ -317,7 +341,10 @@ public class ImportCiphersControllerTests var orgIdGuid = Guid.Parse(orgId); var existingCollections = fixture.CreateMany(2).ToArray(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; sutProvider.GetDependency() .GetProperUserId(Arg.Any()) @@ -375,7 +402,10 @@ public class ImportCiphersControllerTests // Arrange var orgId = Guid.NewGuid(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; SetupUserService(sutProvider, user); @@ -448,7 +478,10 @@ public class ImportCiphersControllerTests // Arrange var orgId = Guid.NewGuid(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; SetupUserService(sutProvider, user); @@ -525,7 +558,11 @@ public class ImportCiphersControllerTests // Arrange var orgId = Guid.NewGuid(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; + SetupUserService(sutProvider, user); // Create new collections @@ -594,7 +631,10 @@ public class ImportCiphersControllerTests // Arrange var orgId = Guid.NewGuid(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; SetupUserService(sutProvider, user); @@ -666,7 +706,10 @@ public class ImportCiphersControllerTests // Arrange var orgId = Guid.NewGuid(); - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency() + .SelfHosted = false; + sutProvider.GetDependency() + .ImportCiphersLimitation = _organizationCiphersLimitations; SetupUserService(sutProvider, user); From 1176b18d4474957f976473cd5bd6888f8857f334 Mon Sep 17 00:00:00 2001 From: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com> Date: Thu, 10 Jul 2025 12:57:22 -0400 Subject: [PATCH 045/326] fix TDE offboarding event type (#6076) --- .../TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs b/src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs index 8ef586ab51..719ff9ce9d 100644 --- a/src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs +++ b/src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs @@ -91,7 +91,7 @@ public class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand user.MasterPasswordHint = hint; await _userRepository.ReplaceAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword); + await _eventService.LogUserEventAsync(user.Id, EventType.User_TdeOffboardingPasswordSet); await _pushService.PushLogOutAsync(user.Id); return IdentityResult.Success; From ca1baa122061dbf2e86d2b5c78374f65d40ff5e2 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:07:57 -0400 Subject: [PATCH 046/326] chore(feature-flag): Adding feature flag for push notifications on locked account --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5c0bd3216e..bb7758c12b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -194,6 +194,7 @@ public static class FeatureFlagKeys public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string WebPush = "web-push"; public const string IpcChannelFramework = "ipc-channel-framework"; + public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; /* Tools Team */ public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; From 067e464ec4e16adbb6a798e85814c0244b704bdd Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Fri, 11 Jul 2025 07:32:59 -0700 Subject: [PATCH 047/326] [PM-23183] Add logger data before throwing for mismatched encryptedFor (#6078) --- src/Api/Vault/Controllers/CiphersController.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index e9a3fac08f..853dadebd0 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -157,6 +157,7 @@ public class CiphersController : Controller { if (model.EncryptedFor != user.Id) { + _logger.LogError("Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", user.Id, model.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -186,6 +187,7 @@ public class CiphersController : Controller { if (model.Cipher.EncryptedFor != user.Id) { + _logger.LogError("Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", user.Id, model.Cipher.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -218,6 +220,7 @@ public class CiphersController : Controller { if (model.Cipher.EncryptedFor != userId) { + _logger.LogError("Cipher was not encrypted for the current user. CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", userId, model.Cipher.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -244,6 +247,7 @@ public class CiphersController : Controller { if (model.EncryptedFor != user.Id) { + _logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", id, user.Id, model.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -281,6 +285,7 @@ public class CiphersController : Controller { if (model.EncryptedFor != userId) { + _logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId}, CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", id, userId, model.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -706,6 +711,7 @@ public class CiphersController : Controller { if (model.Cipher.EncryptedFor != user.Id) { + _logger.LogError("Cipher was not encrypted for the current user. CipherId: {CipherId} CurrentUser: {CurrentUserId}, EncryptedFor: {EncryptedFor}", id, user.Id, model.Cipher.EncryptedFor); throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); } } @@ -1077,6 +1083,7 @@ public class CiphersController : Controller { if (cipher.EncryptedFor.HasValue && cipher.EncryptedFor.Value != userId) { + _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."); } } From 24b7cc417f15685b0ca53d4897ab300d3bf2af84 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Fri, 11 Jul 2025 08:23:51 -0700 Subject: [PATCH 048/326] feat(self-host): [PM-14188] Add option to disable built-in MSSQL container * Add Config Option For Disabling Built In MSSQL Container * fix: flip bool condition and make it nullable * fake commit to kick off an ephemeral environment * Revert "fake commit to kick off an ephemeral environment" This reverts commit 818f65f4d2ba64fb9985852147f0854b21c80c94. * Changed the new setting to not be nullable. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Todd Martin --- util/Setup/Configuration.cs | 3 +++ util/Setup/DockerComposeBuilder.cs | 2 ++ util/Setup/Templates/DockerCompose.hbs | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/util/Setup/Configuration.cs b/util/Setup/Configuration.cs index 00a1ef1eca..d5d0139496 100644 --- a/util/Setup/Configuration.cs +++ b/util/Setup/Configuration.cs @@ -97,6 +97,9 @@ public class Configuration [Description("Enable SCIM")] public bool EnableScim { get; set; } = false; + [Description("Enable Built-In MSSQL Container Generation")] + public bool EnableBuiltInMsSql { get; set; } = true; + [YamlIgnore] public string Domain { diff --git a/util/Setup/DockerComposeBuilder.cs b/util/Setup/DockerComposeBuilder.cs index b5976e90cc..278b929cb8 100644 --- a/util/Setup/DockerComposeBuilder.cs +++ b/util/Setup/DockerComposeBuilder.cs @@ -42,6 +42,7 @@ public class DockerComposeBuilder { public TemplateModel(Context context) { + EnableBuiltInMsSql = context.Config.EnableBuiltInMsSql; MssqlDataDockerVolume = context.Config.DatabaseDockerVolume; EnableKeyConnector = context.Config.EnableKeyConnector; EnableScim = context.Config.EnableScim; @@ -61,6 +62,7 @@ public class DockerComposeBuilder } } + public bool EnableBuiltInMsSql { get; set; } public bool MssqlDataDockerVolume { get; set; } public bool EnableKeyConnector { get; set; } public bool EnableScim { get; set; } diff --git a/util/Setup/Templates/DockerCompose.hbs b/util/Setup/Templates/DockerCompose.hbs index 741e1085f9..b4676668cf 100644 --- a/util/Setup/Templates/DockerCompose.hbs +++ b/util/Setup/Templates/DockerCompose.hbs @@ -14,6 +14,7 @@ ######################################################################### services: +{{#if EnableBuiltInMsSql}} mssql: image: ghcr.io/bitwarden/mssql:{{{CoreVersion}}} container_name: bitwarden-mssql @@ -32,6 +33,7 @@ services: - ../env/uid.env - ../env/mssql.override.env +{{/if}} web: image: ghcr.io/bitwarden/web:{{{WebVersion}}} container_name: bitwarden-web @@ -106,8 +108,10 @@ services: image: ghcr.io/bitwarden/admin:{{{CoreVersion}}} container_name: bitwarden-admin restart: always +{{#if EnableBuiltInMsSql}} depends_on: - mssql +{{/if}} volumes: - ../core:/etc/bitwarden/core - ../ca-certificates:/etc/bitwarden/ca-certificates From 9b65e9f4ccc891184aacdd0c5e1f6c18c605b5e2 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:41:32 -0400 Subject: [PATCH 049/326] [PM-22580] Org/User License Codeownership Move (No logic changes) (#6080) * Moved license models to billing * Moved LicensingService to billing * Moved license command and queries to billing * Moved LicenseController to billing --- src/Admin/Controllers/ToolsController.cs | 2 +- .../OrganizationConnectionsController.cs | 2 +- .../Organizations/OrganizationResponseModel.cs | 1 + src/Api/Billing/Controllers/AccountsController.cs | 1 + .../Controllers/LicensesController.cs | 6 +++--- .../Controllers/OrganizationsController.cs | 3 ++- .../SelfHostedOrganizationLicensesController.cs | 4 ++-- src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs | 2 +- src/Api/Jobs/ValidateOrganizationsJob.cs | 4 ++-- src/Api/Jobs/ValidateUsersJob.cs | 4 ++-- .../Models/Response/SubscriptionResponseModel.cs | 1 + src/Core/AdminConsole/Entities/Organization.cs | 2 +- .../SelfHostedOrganizationDetails.cs | 2 +- .../AdminConsole/Services/IOrganizationService.cs | 1 + .../Implementations/OrganizationService.cs | 2 ++ .../AdminConsole/Services/OrganizationFactory.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 11 +++++++++++ .../{ => Billing}/Models/Business/ILicense.cs | 2 +- .../Models/Business/OrganizationLicense.cs | 15 ++++++++------- .../{ => Billing}/Models/Business/UserLicense.cs | 11 ++++++----- .../Cloud/CloudGetOrganizationLicenseQuery.cs | 6 ++++-- .../Interfaces/IGetOrganizationLicenseQuery.cs | 4 ++-- .../IUpdateOrganizationLicenseCommand.cs | 4 ++-- .../SelfHostedGetOrganizationLicenseQuery.cs | 6 +++--- .../UpdateOrganizationLicenseCommand.cs | 7 ++++--- .../{ => Billing}/Services/ILicensingService.cs | 3 ++- .../Services/Implementations/LicensingService.cs | 14 ++++++++------ .../NoopImplementations/NoopLicensingService.cs | 3 ++- .../OrganizationServiceCollectionExtensions.cs | 10 ---------- src/Core/Services/IUserService.cs | 1 + src/Core/Services/Implementations/UserService.cs | 1 + .../ClientProviders/UserClientProvider.cs | 2 +- src/Identity/IdentityServer/ProfileService.cs | 1 + .../OrganizationConnectionsControllerTests.cs | 4 ++-- .../Controllers/OrganizationsControllerTests.cs | 2 +- test/Api.Test/Utilities/ApiHelpersTests.cs | 2 +- .../Data/SelfHostedOrganizationDetailsTests.cs | 4 ++-- .../OrganizationLicenseCustomization.cs | 4 ++-- .../Business/OrganizationLicenseFileFixtures.cs | 4 ++-- .../Models/Business/OrganizationLicenseTests.cs | 4 ++-- .../CloudGetOrganizationLicenseQueryTests.cs | 7 +++++-- .../SelfHostedGetOrganizationLicenseQueryTests.cs | 8 ++++---- .../UpdateOrganizationLicenseCommandTests.cs | 7 ++++--- .../Services/LicensingServiceTests.cs | 8 ++++---- test/Core.Test/Services/UserServiceTests.cs | 3 ++- 45 files changed, 111 insertions(+), 86 deletions(-) rename src/Api/{ => Billing}/Controllers/LicensesController.cs (95%) rename src/Core/{ => Billing}/Models/Business/ILicense.cs (93%) rename src/Core/{ => Billing}/Models/Business/OrganizationLicense.cs (98%) rename src/Core/{ => Billing}/Models/Business/UserLicense.cs (96%) rename src/Core/{ => Billing}/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs (91%) rename src/Core/{ => Billing}/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs (77%) rename src/Core/{ => Billing}/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs (73%) rename src/Core/{ => Billing}/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs (92%) rename src/Core/{ => Billing}/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs (92%) rename src/Core/{ => Billing}/Services/ILicensingService.cs (91%) rename src/Core/{ => Billing}/Services/Implementations/LicensingService.cs (96%) rename src/Core/{ => Billing}/Services/NoopImplementations/NoopLicensingService.cs (96%) rename test/Core.Test/{ => Billing}/AutoFixture/OrganizationLicenseCustomization.cs (85%) rename test/Core.Test/{ => Billing}/Models/Business/OrganizationLicenseFileFixtures.cs (99%) rename test/Core.Test/{ => Billing}/Models/Business/OrganizationLicenseTests.cs (94%) rename test/Core.Test/{ => Billing}/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs (95%) rename test/Core.Test/{ => Billing}/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs (94%) rename test/Core.Test/{ => Billing}/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs (95%) rename test/Core.Test/{ => Billing}/Services/LicensingServiceTests.cs (92%) diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index a640fe352f..fedb96be46 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -8,9 +8,9 @@ using Bit.Admin.Models; using Bit.Admin.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs index 5da090314b..79ed2ceabe 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs @@ -5,13 +5,13 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index 62d7343509..d532975388 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 10d386641d..9411d454aa 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -5,6 +5,7 @@ using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; using Bit.Core.Models.Business; diff --git a/src/Api/Controllers/LicensesController.cs b/src/Api/Billing/Controllers/LicensesController.cs similarity index 95% rename from src/Api/Controllers/LicensesController.cs rename to src/Api/Billing/Controllers/LicensesController.cs index e735cf3b4b..1a19fd27e0 100644 --- a/src/Api/Controllers/LicensesController.cs +++ b/src/Api/Billing/Controllers/LicensesController.cs @@ -2,18 +2,18 @@ #nullable disable using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api.OrganizationLicenses; -using Bit.Core.Models.Business; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -namespace Bit.Api.Controllers; +namespace Bit.Api.Billing.Controllers; [Route("licenses")] [Authorize("Licensing")] diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index a74974ab46..ca13690b5c 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -11,6 +11,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; @@ -18,7 +20,6 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index a0cc64b9bf..bdd8aaba84 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -5,12 +5,12 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Organizations; using Bit.Api.Utilities; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Models.OrganizationConnectionConfigs; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; diff --git a/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs index 7fa9ff068e..4e94cced03 100644 --- a/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs +++ b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs @@ -1,12 +1,12 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Jobs; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Quartz; diff --git a/src/Api/Jobs/ValidateOrganizationsJob.cs b/src/Api/Jobs/ValidateOrganizationsJob.cs index 8c4225a015..b027b4d049 100644 --- a/src/Api/Jobs/ValidateOrganizationsJob.cs +++ b/src/Api/Jobs/ValidateOrganizationsJob.cs @@ -1,5 +1,5 @@ -using Bit.Core.Jobs; -using Bit.Core.Services; +using Bit.Core.Billing.Services; +using Bit.Core.Jobs; using Quartz; namespace Bit.Api.Jobs; diff --git a/src/Api/Jobs/ValidateUsersJob.cs b/src/Api/Jobs/ValidateUsersJob.cs index be531b47de..351e141113 100644 --- a/src/Api/Jobs/ValidateUsersJob.cs +++ b/src/Api/Jobs/ValidateUsersJob.cs @@ -1,5 +1,5 @@ -using Bit.Core.Jobs; -using Bit.Core.Services; +using Bit.Core.Billing.Services; +using Bit.Core.Jobs; using Quartz; namespace Bit.Api.Jobs; diff --git a/src/Api/Models/Response/SubscriptionResponseModel.cs b/src/Api/Models/Response/SubscriptionResponseModel.cs index b460877d30..7038bee2a7 100644 --- a/src/Api/Models/Response/SubscriptionResponseModel.cs +++ b/src/Api/Models/Response/SubscriptionResponseModel.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.Models.Business; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 274c7f8ddb..9d506b4251 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -4,9 +4,9 @@ using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Utilities; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index 68458b09ec..de28c4ba80 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -6,9 +6,9 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; namespace Bit.Core.Models.Data.Organizations; diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 297184430c..1379c06bc3 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 7035641b46..5daefffea0 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -20,7 +20,9 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs index 3261d89253..2d26e3d156 100644 --- a/src/Core/AdminConsole/Services/OrganizationFactory.cs +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; namespace Bit.Core.AdminConsole.Services; diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 5f1a0668bd..944c29e90f 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,9 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; using Bit.Core.Billing.Payment; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; @@ -29,5 +32,13 @@ public static class ServiceCollectionExtensions services.AddPricingClient(); services.AddTransient(); services.AddPaymentOperations(); + services.AddOrganizationLicenseCommandsQueries(); + } + + private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Billing/Models/Business/ILicense.cs similarity index 93% rename from src/Core/Models/Business/ILicense.cs rename to src/Core/Billing/Models/Business/ILicense.cs index b0e295bdd9..2727541847 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Billing/Models/Business/ILicense.cs @@ -1,6 +1,6 @@ using System.Security.Cryptography.X509Certificates; -namespace Bit.Core.Models.Business; +namespace Bit.Core.Billing.Models.Business; public interface ILicense { diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Billing/Models/Business/OrganizationLicense.cs similarity index 98% rename from src/Core/Models/Business/OrganizationLicense.cs rename to src/Core/Billing/Models/Business/OrganizationLicense.cs index c40f5fd899..2567c274e9 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Billing/Models/Business/OrganizationLicense.cs @@ -10,11 +10,12 @@ using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Services; using Bit.Core.Enums; -using Bit.Core.Services; +using Bit.Core.Models.Business; using Bit.Core.Settings; -namespace Bit.Core.Models.Business; +namespace Bit.Core.Billing.Models.Business; public class OrganizationLicense : ILicense { @@ -54,7 +55,7 @@ public class OrganizationLicense : ILicense ILicensingService licenseService, int? version = null) { Version = version.GetValueOrDefault(CurrentLicenseFileVersion); // TODO: Remember to change the constant - LicenseType = Enums.LicenseType.Organization; + LicenseType = Core.Enums.LicenseType.Organization; LicenseKey = org.LicenseKey; InstallationId = installationId; Id = org.Id; @@ -124,7 +125,7 @@ public class OrganizationLicense : ILicense subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { Refresh = DateTime.UtcNow.AddDays(30); - Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants + Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Core.Constants .OrganizationSelfHostSubscriptionGracePeriodDays); ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate; } @@ -263,7 +264,7 @@ public class OrganizationLicense : ILicense !p.Name.Equals(nameof(UseAdminSponsoredFamilies)) && !p.Name.Equals(nameof(UseOrganizationDomains))) .OrderBy(p => p.Name) - .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") + .Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); data = $"license:organization|{props}"; } @@ -315,7 +316,7 @@ public class OrganizationLicense : ILicense } var licenseType = claimsPrincipal.GetValue(nameof(LicenseType)); - if (licenseType != Enums.LicenseType.Organization) + if (licenseType != Core.Enums.LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + "Upload this license from your personal account settings page."); @@ -396,7 +397,7 @@ public class OrganizationLicense : ILicense errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); } - if (LicenseType != null && LicenseType != Enums.LicenseType.Organization) + if (LicenseType != null && LicenseType != Core.Enums.LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + "Upload this license from your personal account settings page."); diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Billing/Models/Business/UserLicense.cs similarity index 96% rename from src/Core/Models/Business/UserLicense.cs rename to src/Core/Billing/Models/Business/UserLicense.cs index da61369b24..d13de17d47 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Billing/Models/Business/UserLicense.cs @@ -8,11 +8,12 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Services; +using Bit.Core.Models.Business; -namespace Bit.Core.Models.Business; +namespace Bit.Core.Billing.Models.Business; public class UserLicense : ILicense { @@ -22,7 +23,7 @@ public class UserLicense : ILicense public UserLicense(User user, SubscriptionInfo subscriptionInfo, ILicensingService licenseService, int? version = null) { - LicenseType = Enums.LicenseType.User; + LicenseType = Core.Enums.LicenseType.User; LicenseKey = user.LicenseKey; Id = user.Id; Name = user.Name; @@ -44,7 +45,7 @@ public class UserLicense : ILicense public UserLicense(User user, ILicensingService licenseService, int? version = null) { - LicenseType = Enums.LicenseType.User; + LicenseType = Core.Enums.LicenseType.User; LicenseKey = user.LicenseKey; Id = user.Id; Name = user.Name; @@ -100,7 +101,7 @@ public class UserLicense : ILicense ) )) .OrderBy(p => p.Name) - .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") + .Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); data = $"license:user|{props}"; } diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs similarity index 91% rename from src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs rename to src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index 99a8e68380..ff768ee03d 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -3,14 +3,16 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Platform.Installations; using Bit.Core.Services; -namespace Bit.Core.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuery { diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs similarity index 77% rename from src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs rename to src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs index 312b80a466..147bee398f 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs +++ b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; -using Bit.Core.Models.Business; -namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; public interface ICloudGetOrganizationLicenseQuery { diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs similarity index 73% rename from src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs rename to src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs index 78f590e59f..c9c71f37a4 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs +++ b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs @@ -1,10 +1,10 @@ #nullable enable using Bit.Core.AdminConsole.Entities; -using Bit.Core.Models.Business; +using Bit.Core.Billing.Models.Business; using Bit.Core.Models.Data.Organizations; -namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; public interface IUpdateOrganizationLicenseCommand { diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs similarity index 92% rename from src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs rename to src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs index 7c78abe480..8448554f24 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs +++ b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs @@ -2,18 +2,18 @@ #nullable disable using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.OrganizationLicenses; -using Bit.Core.Models.Business; using Bit.Core.Models.OrganizationConnectionConfigs; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -namespace Bit.Core.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; public class SelfHostedGetOrganizationLicenseQuery : BaseIdentityClientService, ISelfHostedGetOrganizationLicenseQuery { diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs similarity index 92% rename from src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs rename to src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs index ffeee39c07..364571437a 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs @@ -2,15 +2,16 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Services; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -namespace Bit.Core.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseCommand { diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Billing/Services/ILicensingService.cs similarity index 91% rename from src/Core/Services/ILicensingService.cs rename to src/Core/Billing/Services/ILicensingService.cs index 2115e43085..3b7ac2580d 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Billing/Services/ILicensingService.cs @@ -2,10 +2,11 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Models.Business; -namespace Bit.Core.Services; +namespace Bit.Core.Billing.Services; public interface ILicensingService { diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Billing/Services/Implementations/LicensingService.cs similarity index 96% rename from src/Core/Services/Implementations/LicensingService.cs rename to src/Core/Billing/Services/Implementations/LicensingService.cs index ca607bb5b4..cdb330fe9b 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Billing/Services/Implementations/LicensingService.cs @@ -9,10 +9,12 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Licenses.Models; using Bit.Core.Billing.Licenses.Services; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using IdentityModel; @@ -21,7 +23,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; -namespace Bit.Core.Services; +namespace Bit.Core.Billing.Services; public class LicensingService : ILicensingService { @@ -94,7 +96,7 @@ public class LicensingService : ILicensingService } var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); - _logger.LogInformation(Constants.BypassFiltersEventId, null, + _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, "Validating licenses for {NumberOfOrganizations} organizations.", enabledOrgs.Count); var exceptions = new List(); @@ -143,7 +145,7 @@ public class LicensingService : ILicensingService private async Task DisableOrganizationAsync(Organization org, ILicense license, string reason) { - _logger.LogInformation(Constants.BypassFiltersEventId, null, + _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, "Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}", org.Id, org.DisplayName(), reason); org.Enabled = false; @@ -162,7 +164,7 @@ public class LicensingService : ILicensingService } var premiumUsers = await _userRepository.GetManyByPremiumAsync(true); - _logger.LogInformation(Constants.BypassFiltersEventId, null, + _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, "Validating premium for {0} users.", premiumUsers.Count); foreach (var user in premiumUsers) @@ -201,7 +203,7 @@ public class LicensingService : ILicensingService _userCheckCache.Add(user.Id, now); } - _logger.LogInformation(Constants.BypassFiltersEventId, null, + _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, "Validating premium license for user {0}({1}).", user.Id, user.Email); return await ProcessUserValidationAsync(user); } @@ -233,7 +235,7 @@ public class LicensingService : ILicensingService private async Task DisablePremiumAsync(User user, ILicense license, string reason) { - _logger.LogInformation(Constants.BypassFiltersEventId, null, + _logger.LogInformation(Core.Constants.BypassFiltersEventId, null, "User {0}({1}) has an invalid license and premium is being disabled. Reason: {2}", user.Id, user.Email, reason); diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs similarity index 96% rename from src/Core/Services/NoopImplementations/NoopLicensingService.cs rename to src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs index b181e61138..8f57d7b879 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs @@ -2,13 +2,14 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Settings; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Bit.Core.Services; +namespace Bit.Core.Billing.Services; public class NoopLicensingService : ILicensingService { diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index ef78e966f6..ac1fe262c2 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -22,8 +22,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v using Bit.Core.Models.Business.Tokenables; using Bit.Core.OrganizationFeatures.OrganizationCollections; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; -using Bit.Core.OrganizationFeatures.OrganizationLicenses; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -56,7 +54,6 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationApiKeyCommandsQueries(); services.AddOrganizationCollectionCommands(); services.AddOrganizationGroupCommands(); - services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); @@ -157,13 +154,6 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } - private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - } - private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 43c204e513..8457a9c128 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -5,6 +5,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 9ae10e333f..0da565c4ba 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -16,6 +16,7 @@ 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.Services; using Bit.Core.Billing.Tax.Models; diff --git a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs index 82abfa3536..57699ae415 100644 --- a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs @@ -3,10 +3,10 @@ using System.Collections.ObjectModel; using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Identity; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Utilities; using Duende.IdentityServer.Models; using IdentityModel; diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index c1230f9694..742e69758b 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Identity; using Bit.Core.Repositories; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs index dff61aa2b4..a5d00339c1 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs @@ -4,14 +4,14 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 63afb2a7a8..71bef89152 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; @@ -19,7 +20,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Api.Test/Utilities/ApiHelpersTests.cs b/test/Api.Test/Utilities/ApiHelpersTests.cs index 4013a2222a..771a4681bb 100644 --- a/test/Api.Test/Utilities/ApiHelpersTests.cs +++ b/test/Api.Test/Utilities/ApiHelpersTests.cs @@ -1,6 +1,6 @@ using System.Text; using Bit.Api.Utilities; -using Bit.Core.Models.Business; +using Bit.Core.Billing.Models.Business; using Microsoft.AspNetCore.Http; using NSubstitute; using Xunit; diff --git a/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs b/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs index f2fac4aceb..fc11659fee 100644 --- a/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs @@ -5,11 +5,11 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; -using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.Billing.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; diff --git a/test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs b/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs similarity index 85% rename from test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs rename to test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs index 66a7f52249..bc0aeae98d 100644 --- a/test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs +++ b/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs @@ -1,8 +1,8 @@ using AutoFixture; -using Bit.Core.Models.Business; +using Bit.Core.Billing.Models.Business; using Bit.Test.Common.AutoFixture.Attributes; -namespace Bit.Core.Test.AutoFixture; +namespace Bit.Core.Test.Billing.AutoFixture; public class OrganizationLicenseCustomizeAttribute : BitCustomizeAttribute { diff --git a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs similarity index 99% rename from test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs rename to test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs index 08771df06a..44ab82a04d 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs @@ -1,10 +1,10 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.Business; using Bit.Core.Enums; -using Bit.Core.Models.Business; -namespace Bit.Core.Test.Models.Business; +namespace Bit.Core.Test.Billing.Models.Business; /// /// Contains test data for OrganizationLicense tests, including json strings for each OrganizationLicense version. diff --git a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs similarity index 94% rename from test/Core.Test/Models/Business/OrganizationLicenseTests.cs rename to test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs index 836df59dd8..0be72a5374 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs @@ -1,11 +1,11 @@ using System.Security.Claims; -using Bit.Core.Models.Business; +using Bit.Core.Billing.Models.Business; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Models.Business; +namespace Bit.Core.Test.Billing.Models.Business; public class OrganizationLicenseTests { diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs similarity index 95% rename from test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index cc8ab956ca..a92ef72a13 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -1,13 +1,16 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; -using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.Platform.Installations; using Bit.Core.Services; using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.Billing.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -15,7 +18,7 @@ using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; -namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; [SubscriptionInfoCustomize] [OrganizationLicenseCustomize] diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs similarity index 94% rename from test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs index 0e5b73d112..3fb960aa94 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs @@ -1,18 +1,18 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; using Bit.Core.Entities; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Models.OrganizationConnectionConfigs; -using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.Settings; -using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.Billing.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Xunit; -namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; [SutProviderCustomize] public class SelfHostedGetOrganizationLicenseQueryTests diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs similarity index 95% rename from test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs rename to test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 5ad6abd26a..4b11cddb35 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -1,9 +1,10 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Billing.Services; using Bit.Core.Enums; -using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; -using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; @@ -13,7 +14,7 @@ using NSubstitute; using Xunit; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; [SutProviderCustomize] public class UpdateOrganizationLicenseCommandTests diff --git a/test/Core.Test/Services/LicensingServiceTests.cs b/test/Core.Test/Billing/Services/LicensingServiceTests.cs similarity index 92% rename from test/Core.Test/Services/LicensingServiceTests.cs rename to test/Core.Test/Billing/Services/LicensingServiceTests.cs index 3e8b1735e2..1039f0bbfb 100644 --- a/test/Core.Test/Services/LicensingServiceTests.cs +++ b/test/Core.Test/Billing/Services/LicensingServiceTests.cs @@ -1,15 +1,15 @@ using System.Text.Json; using AutoFixture; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Models.Business; -using Bit.Core.Services; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Services; using Bit.Core.Settings; -using Bit.Core.Test.AutoFixture; +using Bit.Core.Test.Billing.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Services; +namespace Bit.Core.Test.Billing.Services; [SutProviderCustomize] public class LicensingServiceTests diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 5332ae21de..9d83674f44 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -11,10 +11,11 @@ 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.Models.Business; +using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; From 2f8460f4db709edcc7d2300d9f6e84a2deb4c6bb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:23:30 -0400 Subject: [PATCH 050/326] feat(OTP): [PM-18612] Change email OTP to six digits * Change email OTP to 6 digits * Added comment on base class * Added tests * Renamed tests. * Fixed tests * Renamed file to match class --- .../TokenProviders/EmailTokenProvider.cs | 17 ++++- .../EmailTwoFactorTokenProvider.cs | 12 +++- src/Core/Constants.cs | 1 + ...henticationTwoFactorTokenProviderTests.cs} | 2 +- ....cs => BaseTwoFactorTokenProviderTests.cs} | 2 +- ...DuoUniversalTwoFactorTokenProviderTests.cs | 2 +- .../Auth/Identity/EmailTokenProviderTests.cs | 68 +++++++++++++++++++ .../EmailTwoFactorTokenProviderTests.cs | 48 ++++++++++++- 8 files changed, 144 insertions(+), 8 deletions(-) rename test/Core.Test/Auth/Identity/{AuthenticationTokenProviderTests.cs => AuthenticationTwoFactorTokenProviderTests.cs} (91%) rename test/Core.Test/Auth/Identity/{BaseTokenProviderTests.cs => BaseTwoFactorTokenProviderTests.cs} (98%) create mode 100644 test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index be94124c03..70aba8ef75 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -1,5 +1,6 @@ using System.Text; using Bit.Core.Entities; +using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; @@ -7,6 +8,9 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; +/// +/// Generates and validates tokens for email OTPs. +/// public class EmailTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "EmailToken_{0}_{1}_{2}"; @@ -16,16 +20,25 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider public EmailTokenProvider( [FromKeyedServices("persistent")] - IDistributedCache distributedCache) + IDistributedCache distributedCache, + IFeatureService featureService) { _distributedCache = distributedCache; _distributedCacheEntryOptions = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; + if (featureService.IsEnabled(FeatureFlagKeys.Otp6Digits)) + { + TokenLength = 6; + } + else + { + TokenLength = 8; + } } - public int TokenLength { get; protected set; } = 8; + public int TokenLength { get; protected set; } public bool TokenAlpha { get; protected set; } = false; public bool TokenNumeric { get; protected set; } = true; diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index c4b4c1d2ca..2d72781569 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -4,19 +4,27 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; +using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; +/// +/// Generates tokens for email two-factor authentication. +/// It inherits from the EmailTokenProvider class, which manages the persistence and validation of tokens, +/// and adds additional validation to ensure that 2FA is enabled for the user. +/// public class EmailTwoFactorTokenProvider : EmailTokenProvider { public EmailTwoFactorTokenProvider( [FromKeyedServices("persistent")] - IDistributedCache distributedCache) : - base(distributedCache) + IDistributedCache distributedCache, + IFeatureService featureService) : + base(distributedCache, featureService) { + // This can be removed when the pm-18612-otp-6-digits feature flag is removed because the base implementation will match. TokenAlpha = false; TokenNumeric = true; TokenLength = 6; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index bb7758c12b..2ef9a20ae3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -124,6 +124,7 @@ public static class FeatureFlagKeys public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; + public const string Otp6Digits = "pm-18612-otp-6-digits"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; diff --git a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs b/test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs similarity index 91% rename from test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs rename to test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs index c9646e627c..b3f7f4f1c4 100644 --- a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs @@ -7,7 +7,7 @@ using Xunit; namespace Bit.Core.Test.Auth.Identity; -public class AuthenticationTokenProviderTests : BaseTokenProviderTests +public class AuthenticationTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Authenticator; diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs similarity index 98% rename from test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs rename to test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs index ff09e1f141..04cbe026ba 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs @@ -14,7 +14,7 @@ using Xunit; namespace Bit.Core.Test.Auth.Identity; [SutProviderCustomize] -public abstract class BaseTokenProviderTests +public abstract class BaseTwoFactorTokenProviderTests where T : IUserTwoFactorTokenProvider { public abstract TwoFactorProviderType TwoFactorProviderType { get; } diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs index 5715403974..99d2dd3938 100644 --- a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -12,7 +12,7 @@ using Duo = DuoUniversal; namespace Bit.Core.Test.Auth.Identity; -public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests +public class DuoUniversalTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For(); public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo; diff --git a/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs new file mode 100644 index 0000000000..f29b11b935 --- /dev/null +++ b/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs @@ -0,0 +1,68 @@ +using Bit.Core; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +public class EmailTokenProviderTests +{ + private readonly IDistributedCache _cache; + + public EmailTokenProviderTests() + { + _cache = Substitute.For(); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_GeneratesSixDigitToken_WhenFeatureFlagIsEnabled(User user) + { + // Arrange + var purpose = "test-purpose"; + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(true); + var tokenProvider = new EmailTokenProvider(_cache, featureService); + + // Act + var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user); + + // Assert + Assert.Equal(6, code.Length); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_GeneratesEightDigitToken_WhenFeatureFlagIsDisabled(User user) + { + // Arrange + var purpose = "test-purpose"; + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(false); + var tokenProvider = new EmailTokenProvider(_cache, featureService); + // Act + var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user); + + // Assert + Assert.Equal(8, code.Length); + } + + protected static UserManager SubstituteUserManager() + { + return new UserManager(Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Enumerable.Empty>(), + Enumerable.Empty>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>>()); + } +} diff --git a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs index 46bfba549e..aa801dce58 100644 --- a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs @@ -1,13 +1,15 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; +using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; using Xunit; namespace Bit.Core.Test.Auth.Identity; -public class EmailTwoFactorTokenProviderTests : BaseTokenProviderTests +public class EmailTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Email; @@ -42,4 +44,48 @@ public class EmailTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorEmailProvidersJson(); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.Otp6Digits) + .Returns(true); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.NotNull(token); + Assert.Equal(6, token.Length); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_ShouldReturnSixDigitToken_WithFeatureFlagDisabled( + User user, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorEmailProvidersJson(); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.Otp6Digits) + .Returns(false); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.NotNull(token); + Assert.Equal(6, token.Length); + } + + private string GetTwoFactorEmailProvidersJson() + { + return + "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"test@email.com\"}}}"; + } } From 0e4e060f225adbb557439d3a279235cd708e80fc Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 14 Jul 2025 14:29:17 +0000 Subject: [PATCH 051/326] Bumped version to 2025.7.1 --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c080e2eff4..fb86d5f089 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.7.0 + 2025.7.1 Bit.$(MSBuildProjectName) enable @@ -68,4 +68,4 @@ - + \ No newline at end of file From d914ab8a988b3aee0113f5afbb70ac36675b7d72 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:39:49 -0500 Subject: [PATCH 052/326] [PM-23687] Support free organizations on Payment Details page (#6084) * Resolve JSON serialization bug in OneOf converters and organize pricing models * Support free organizations for payment method and billing address flows * Run dotnet format --- ...illingCommand.cs => BaseBillingCommand.cs} | 31 +++- src/Core/Billing/Constants/StripeConstants.cs | 1 + .../CreateBitPayInvoiceForCreditCommand.cs | 7 +- .../Commands/UpdateBillingAddressCommand.cs | 24 +++- .../Commands/UpdatePaymentMethodCommand.cs | 17 ++- .../Commands/VerifyBankAccountCommand.cs | 11 +- .../Billing/Payment/Models/BillingAddress.cs | 3 +- .../Payment/Models/MaskedPaymentMethod.cs | 40 +++--- .../Payment/Models/TokenizedPaymentMethod.cs | 3 +- .../Payment/Queries/GetBillingAddressQuery.cs | 3 +- .../Billing/Payment/Queries/GetCreditQuery.cs | 3 +- .../Payment/Queries/GetPaymentMethodQuery.cs | 8 +- .../JSON/FreeOrScalableDTOJsonConverter.cs | 35 ----- .../JSON/PurchasableDTOJsonConverter.cs | 40 ------ .../Pricing/JSON/TypeReadingJsonConverter.cs | 36 ----- src/Core/Billing/Pricing/Models/Feature.cs | 7 + src/Core/Billing/Pricing/Models/FeatureDTO.cs | 9 -- src/Core/Billing/Pricing/Models/Plan.cs | 25 ++++ src/Core/Billing/Pricing/Models/PlanDTO.cs | 27 ---- .../Billing/Pricing/Models/Purchasable.cs | 135 ++++++++++++++++++ .../Billing/Pricing/Models/PurchasableDTO.cs | 73 ---------- src/Core/Billing/Pricing/PlanAdapter.cs | 40 +++--- src/Core/Billing/Pricing/PricingClient.cs | 5 +- .../Billing/Services/ISubscriberService.cs | 3 + .../Implementations/SubscriberService.cs | 113 ++++++++++++++- .../Tax/Commands/PreviewTaxAmountCommand.cs | 8 +- .../UpdateBillingAddressCommandTests.cs | 66 ++++++++- .../UpdatePaymentMethodCommandTests.cs | 79 +++++++++- .../Models/MaskedPaymentMethodTests.cs | 21 +++ .../Queries/GetPaymentMethodQueryTests.cs | 18 +++ 30 files changed, 575 insertions(+), 316 deletions(-) rename src/Core/Billing/Commands/{BillingCommand.cs => BaseBillingCommand.cs} (60%) delete mode 100644 src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs delete mode 100644 src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs delete mode 100644 src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs create mode 100644 src/Core/Billing/Pricing/Models/Feature.cs delete mode 100644 src/Core/Billing/Pricing/Models/FeatureDTO.cs create mode 100644 src/Core/Billing/Pricing/Models/Plan.cs delete mode 100644 src/Core/Billing/Pricing/Models/PlanDTO.cs create mode 100644 src/Core/Billing/Pricing/Models/Purchasable.cs delete mode 100644 src/Core/Billing/Pricing/Models/PurchasableDTO.cs diff --git a/src/Core/Billing/Commands/BillingCommand.cs b/src/Core/Billing/Commands/BaseBillingCommand.cs similarity index 60% rename from src/Core/Billing/Commands/BillingCommand.cs rename to src/Core/Billing/Commands/BaseBillingCommand.cs index e6c6375b62..b3e938548d 100644 --- a/src/Core/Billing/Commands/BillingCommand.cs +++ b/src/Core/Billing/Commands/BaseBillingCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Constants; +using Bit.Core.Exceptions; using Microsoft.Extensions.Logging; using Stripe; @@ -6,11 +7,17 @@ namespace Bit.Core.Billing.Commands; using static StripeConstants; -public abstract class BillingCommand( +public abstract class BaseBillingCommand( ILogger logger) { protected string CommandName => GetType().Name; + /// + /// Override this property to set a client-facing conflict response in the case a is thrown + /// during the command's execution. + /// + protected virtual Conflict? DefaultConflict => null; + /// /// Executes the provided function within a predefined execution context, handling any exceptions that occur during the process. /// @@ -29,23 +36,35 @@ public abstract class BillingCommand( return stripeException.StripeError.Code switch { ErrorCodes.CustomerTaxLocationInvalid => - new BadRequest("Your location wasn't recognized. Please ensure your country and postal code are valid and try again."), + new BadRequest( + "Your location wasn't recognized. Please ensure your country and postal code are valid and try again."), ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => - new BadRequest("You have exceeded the number of allowed verification attempts. Please contact support for assistance."), + new BadRequest( + "You have exceeded the number of allowed verification attempts. Please contact support for assistance."), ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => - new BadRequest("The verification code you provided does not match the one sent to your bank account. Please try again."), + new BadRequest( + "The verification code you provided does not match the one sent to your bank account. Please try again."), ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => - new BadRequest("Your bank account was not verified within the required time period. Please contact support for assistance."), + new BadRequest( + "Your bank account was not verified within the required time period. Please contact support for assistance."), ErrorCodes.TaxIdInvalid => - new BadRequest("The tax ID number you provided was invalid. Please try again or contact support for assistance."), + new BadRequest( + "The tax ID number you provided was invalid. Please try again or contact support for assistance."), _ => new Unhandled(stripeException) }; } + catch (ConflictException conflictException) + { + logger.LogError("{Command}: {Message}", CommandName, conflictException.Message); + return DefaultConflict != null ? + DefaultConflict : + new Unhandled(conflictException); + } catch (StripeException stripeException) { logger.LogError(stripeException, diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 3aaa519d66..6ecfb4d28b 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -60,6 +60,7 @@ public static class StripeConstants public const string InvoiceApproved = "invoice_approved"; public const string OrganizationId = "organizationId"; public const string ProviderId = "providerId"; + public const string Region = "region"; public const string RetiredBraintreeCustomerId = "btCustomerId_old"; public const string UserId = "userId"; } diff --git a/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs b/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs index f61fa9d0f9..a86f0e3ada 100644 --- a/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs +++ b/src/Core/Billing/Payment/Commands/CreateBitPayInvoiceForCreditCommand.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Payment.Clients; @@ -21,8 +20,10 @@ public interface ICreateBitPayInvoiceForCreditCommand public class CreateBitPayInvoiceForCreditCommand( IBitPayClient bitPayClient, GlobalSettings globalSettings, - ILogger logger) : BillingCommand(logger), ICreateBitPayInvoiceForCreditCommand + ILogger logger) : BaseBillingCommand(logger), ICreateBitPayInvoiceForCreditCommand { + protected override Conflict DefaultConflict => new("We had a problem applying your account credit. Please contact support for assistance."); + public Task> Run( ISubscriber subscriber, decimal amount, diff --git a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs index adc534bd7d..fdf519523a 100644 --- a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs +++ b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs @@ -1,8 +1,8 @@ -#nullable enable -using Bit.Core.Billing.Commands; +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; using Bit.Core.Services; using Microsoft.Extensions.Logging; @@ -19,14 +19,26 @@ public interface IUpdateBillingAddressCommand public class UpdateBillingAddressCommand( ILogger logger, - IStripeAdapter stripeAdapter) : BillingCommand(logger), IUpdateBillingAddressCommand + ISubscriberService subscriberService, + IStripeAdapter stripeAdapter) : BaseBillingCommand(logger), IUpdateBillingAddressCommand { + protected override Conflict DefaultConflict => + new("We had a problem updating your billing address. Please contact support for assistance."); + public Task> Run( ISubscriber subscriber, - BillingAddress billingAddress) => HandleAsync(() => subscriber.GetProductUsageType() switch + BillingAddress billingAddress) => HandleAsync(async () => { - ProductUsageType.Personal => UpdatePersonalBillingAddressAsync(subscriber, billingAddress), - ProductUsageType.Business => UpdateBusinessBillingAddressAsync(subscriber, billingAddress) + if (string.IsNullOrEmpty(subscriber.GatewayCustomerId)) + { + await subscriberService.CreateStripeCustomer(subscriber); + } + + return subscriber.GetProductUsageType() switch + { + ProductUsageType.Personal => await UpdatePersonalBillingAddressAsync(subscriber, billingAddress), + ProductUsageType.Business => await UpdateBusinessBillingAddressAsync(subscriber, billingAddress) + }; }); private async Task> UpdatePersonalBillingAddressAsync( diff --git a/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs b/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs index cda685d520..81206b8032 100644 --- a/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs +++ b/src/Core/Billing/Payment/Commands/UpdatePaymentMethodCommand.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Payment.Models; @@ -29,16 +28,22 @@ public class UpdatePaymentMethodCommand( ILogger logger, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService) : BillingCommand(logger), IUpdatePaymentMethodCommand + ISubscriberService subscriberService) : BaseBillingCommand(logger), IUpdatePaymentMethodCommand { private readonly ILogger _logger = logger; - private static readonly Conflict _conflict = new("We had a problem updating your payment method. Please contact support for assistance."); + protected override Conflict DefaultConflict + => new("We had a problem updating your payment method. Please contact support for assistance."); public Task> Run( ISubscriber subscriber, TokenizedPaymentMethod paymentMethod, BillingAddress? billingAddress) => HandleAsync(async () => { + if (string.IsNullOrEmpty(subscriber.GatewayCustomerId)) + { + await subscriberService.CreateStripeCustomer(subscriber); + } + var customer = await subscriberService.GetCustomer(subscriber); var result = paymentMethod.Type switch @@ -80,10 +85,10 @@ public class UpdatePaymentMethodCommand( { case 0: _logger.LogError("{Command}: Could not find setup intent for subscriber's ({SubscriberID}) bank account", CommandName, subscriber.Id); - return _conflict; + return DefaultConflict; case > 1: _logger.LogError("{Command}: Found more than one set up intent for subscriber's ({SubscriberID}) bank account", CommandName, subscriber.Id); - return _conflict; + return DefaultConflict; } var setupIntent = setupIntents.First(); diff --git a/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs b/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs index 1e9492b876..4f3e38707c 100644 --- a/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs +++ b/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Commands; using Bit.Core.Billing.Payment.Models; using Bit.Core.Entities; @@ -19,12 +18,12 @@ public interface IVerifyBankAccountCommand public class VerifyBankAccountCommand( ILogger logger, ISetupIntentCache setupIntentCache, - IStripeAdapter stripeAdapter) : BillingCommand(logger), IVerifyBankAccountCommand + IStripeAdapter stripeAdapter) : BaseBillingCommand(logger), IVerifyBankAccountCommand { private readonly ILogger _logger = logger; - private static readonly Conflict _conflict = - new("We had a problem verifying your bank account. Please contact support for assistance."); + protected override Conflict DefaultConflict + => new("We had a problem verifying your bank account. Please contact support for assistance."); public Task> Run( ISubscriber subscriber, @@ -37,7 +36,7 @@ public class VerifyBankAccountCommand( _logger.LogError( "{Command}: Could not find setup intent to verify subscriber's ({SubscriberID}) bank account", CommandName, subscriber.Id); - return _conflict; + return DefaultConflict; } await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, diff --git a/src/Core/Billing/Payment/Models/BillingAddress.cs b/src/Core/Billing/Payment/Models/BillingAddress.cs index 5c2c43231c..39dd1f4121 100644 --- a/src/Core/Billing/Payment/Models/BillingAddress.cs +++ b/src/Core/Billing/Payment/Models/BillingAddress.cs @@ -1,5 +1,4 @@ -#nullable enable -using Stripe; +using Stripe; namespace Bit.Core.Billing.Payment.Models; diff --git a/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs index c98fddc785..d23ca75025 100644 --- a/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs +++ b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs @@ -1,7 +1,5 @@ -#nullable enable -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -using Bit.Core.Billing.Pricing.JSON; using Braintree; using OneOf; using Stripe; @@ -83,32 +81,28 @@ public class MaskedPaymentMethod(OneOf new MaskedPayPalAccount { Email = payPalAccount.Email }; } -public class MaskedPaymentMethodJsonConverter : TypeReadingJsonConverter +public class MaskedPaymentMethodJsonConverter : JsonConverter { - protected override string TypePropertyName => nameof(MaskedBankAccount.Type).ToLower(); + private const string _typePropertyName = nameof(MaskedBankAccount.Type); - public override MaskedPaymentMethod? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override MaskedPaymentMethod Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var type = ReadType(reader); + var element = JsonElement.ParseValue(ref reader); + + if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty)) + { + throw new JsonException( + $"Failed to deserialize {nameof(MaskedPaymentMethod)}: missing '{_typePropertyName}' property"); + } + + var type = typeProperty.GetString(); return type switch { - "bankAccount" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var bankAccount => new MaskedPaymentMethod(bankAccount) - }, - "card" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var card => new MaskedPaymentMethod(card) - }, - "payPal" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var payPal => new MaskedPaymentMethod(payPal) - }, - _ => Skip(ref reader) + "bankAccount" => element.Deserialize(options)!, + "card" => element.Deserialize(options)!, + "payPal" => element.Deserialize(options)!, + _ => throw new JsonException($"Failed to deserialize {nameof(MaskedPaymentMethod)}: invalid '{_typePropertyName}' value - '{type}'") }; } diff --git a/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs b/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs index edbf1bb121..9af7c9888a 100644 --- a/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs +++ b/src/Core/Billing/Payment/Models/TokenizedPaymentMethod.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Bit.Core.Billing.Payment.Models; +namespace Bit.Core.Billing.Payment.Models; public record TokenizedPaymentMethod { diff --git a/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs b/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs index 84d4d4f377..e49c2cc993 100644 --- a/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs +++ b/src/Core/Billing/Payment/Queries/GetBillingAddressQuery.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Services; using Bit.Core.Entities; diff --git a/src/Core/Billing/Payment/Queries/GetCreditQuery.cs b/src/Core/Billing/Payment/Queries/GetCreditQuery.cs index 79c9a13aba..81d560269b 100644 --- a/src/Core/Billing/Payment/Queries/GetCreditQuery.cs +++ b/src/Core/Billing/Payment/Queries/GetCreditQuery.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Services; using Bit.Core.Entities; namespace Bit.Core.Billing.Payment.Queries; diff --git a/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs index eb42a8c78a..ce8f031a5d 100644 --- a/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs +++ b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; @@ -29,6 +28,11 @@ public class GetPaymentMethodQuery( var customer = await subscriberService.GetCustomer(subscriber, new CustomerGetOptions { Expand = ["default_source", "invoice_settings.default_payment_method"] }); + if (customer == null) + { + return null; + } + if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId)) { var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId); diff --git a/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs b/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs deleted file mode 100644 index 37a8a4234d..0000000000 --- a/src/Core/Billing/Pricing/JSON/FreeOrScalableDTOJsonConverter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Text.Json; -using Bit.Core.Billing.Pricing.Models; - -namespace Bit.Core.Billing.Pricing.JSON; - -#nullable enable - -public class FreeOrScalableDTOJsonConverter : TypeReadingJsonConverter -{ - public override FreeOrScalableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var type = ReadType(reader); - - return type switch - { - "free" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var free => new FreeOrScalableDTO(free) - }, - "scalable" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var scalable => new FreeOrScalableDTO(scalable) - }, - _ => null - }; - } - - public override void Write(Utf8JsonWriter writer, FreeOrScalableDTO value, JsonSerializerOptions options) - => value.Switch( - free => JsonSerializer.Serialize(writer, free, options), - scalable => JsonSerializer.Serialize(writer, scalable, options) - ); -} diff --git a/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs b/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs deleted file mode 100644 index f7ae9dc472..0000000000 --- a/src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json; -using Bit.Core.Billing.Pricing.Models; - -namespace Bit.Core.Billing.Pricing.JSON; - -#nullable enable -internal class PurchasableDTOJsonConverter : TypeReadingJsonConverter -{ - public override PurchasableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var type = ReadType(reader); - - return type switch - { - "free" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var free => new PurchasableDTO(free) - }, - "packaged" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var packaged => new PurchasableDTO(packaged) - }, - "scalable" => JsonSerializer.Deserialize(ref reader, options) switch - { - null => null, - var scalable => new PurchasableDTO(scalable) - }, - _ => null - }; - } - - public override void Write(Utf8JsonWriter writer, PurchasableDTO value, JsonSerializerOptions options) - => value.Switch( - free => JsonSerializer.Serialize(writer, free, options), - packaged => JsonSerializer.Serialize(writer, packaged, options), - scalable => JsonSerializer.Serialize(writer, scalable, options) - ); -} diff --git a/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs b/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs deleted file mode 100644 index 05beccdb60..0000000000 --- a/src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Bit.Core.Billing.Pricing.Models; - -namespace Bit.Core.Billing.Pricing.JSON; - -#nullable enable - -public abstract class TypeReadingJsonConverter : JsonConverter where T : class -{ - protected virtual string TypePropertyName => nameof(ScalableDTO.Type).ToLower(); - - protected string? ReadType(Utf8JsonReader reader) - { - while (reader.Read()) - { - if (reader.CurrentDepth != 1 || - reader.TokenType != JsonTokenType.PropertyName || - reader.GetString()?.ToLower() != TypePropertyName) - { - continue; - } - - reader.Read(); - return reader.GetString(); - } - - return null; - } - - protected T? Skip(ref Utf8JsonReader reader) - { - reader.Skip(); - return null; - } -} diff --git a/src/Core/Billing/Pricing/Models/Feature.cs b/src/Core/Billing/Pricing/Models/Feature.cs new file mode 100644 index 0000000000..ea9da5217d --- /dev/null +++ b/src/Core/Billing/Pricing/Models/Feature.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Pricing.Models; + +public class Feature +{ + public required string Name { get; set; } + public required string LookupKey { get; set; } +} diff --git a/src/Core/Billing/Pricing/Models/FeatureDTO.cs b/src/Core/Billing/Pricing/Models/FeatureDTO.cs deleted file mode 100644 index a96ac019e3..0000000000 --- a/src/Core/Billing/Pricing/Models/FeatureDTO.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Core.Billing.Pricing.Models; - -#nullable enable - -public class FeatureDTO -{ - public string Name { get; set; } = null!; - public string LookupKey { get; set; } = null!; -} diff --git a/src/Core/Billing/Pricing/Models/Plan.cs b/src/Core/Billing/Pricing/Models/Plan.cs new file mode 100644 index 0000000000..5b4296474b --- /dev/null +++ b/src/Core/Billing/Pricing/Models/Plan.cs @@ -0,0 +1,25 @@ +namespace Bit.Core.Billing.Pricing.Models; + +public class Plan +{ + public required string LookupKey { get; set; } + public required string Name { get; set; } + public required string Tier { get; set; } + public string? Cadence { get; set; } + public int? LegacyYear { get; set; } + public bool Available { get; set; } + public required Feature[] Features { get; set; } + public required Purchasable Seats { get; set; } + public Scalable? ManagedSeats { get; set; } + public Scalable? Storage { get; set; } + public SecretsManagerPurchasables? SecretsManager { get; set; } + public int? TrialPeriodDays { get; set; } + public required string[] CanUpgradeTo { get; set; } + public required Dictionary AdditionalData { get; set; } +} + +public class SecretsManagerPurchasables +{ + public required FreeOrScalable Seats { get; set; } + public required FreeOrScalable ServiceAccounts { get; set; } +} diff --git a/src/Core/Billing/Pricing/Models/PlanDTO.cs b/src/Core/Billing/Pricing/Models/PlanDTO.cs deleted file mode 100644 index 4ae82b3efe..0000000000 --- a/src/Core/Billing/Pricing/Models/PlanDTO.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Bit.Core.Billing.Pricing.Models; - -#nullable enable - -public class PlanDTO -{ - public string LookupKey { get; set; } = null!; - public string Name { get; set; } = null!; - public string Tier { get; set; } = null!; - public string? Cadence { get; set; } - public int? LegacyYear { get; set; } - public bool Available { get; set; } - public FeatureDTO[] Features { get; set; } = null!; - public PurchasableDTO Seats { get; set; } = null!; - public ScalableDTO? ManagedSeats { get; set; } - public ScalableDTO? Storage { get; set; } - public SecretsManagerPurchasablesDTO? SecretsManager { get; set; } - public int? TrialPeriodDays { get; set; } - public string[] CanUpgradeTo { get; set; } = null!; - public Dictionary AdditionalData { get; set; } = null!; -} - -public class SecretsManagerPurchasablesDTO -{ - public FreeOrScalableDTO Seats { get; set; } = null!; - public FreeOrScalableDTO ServiceAccounts { get; set; } = null!; -} diff --git a/src/Core/Billing/Pricing/Models/Purchasable.cs b/src/Core/Billing/Pricing/Models/Purchasable.cs new file mode 100644 index 0000000000..7cb4ee00c1 --- /dev/null +++ b/src/Core/Billing/Pricing/Models/Purchasable.cs @@ -0,0 +1,135 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using OneOf; + +namespace Bit.Core.Billing.Pricing.Models; + +[JsonConverter(typeof(PurchasableJsonConverter))] +public class Purchasable(OneOf input) : OneOfBase(input) +{ + public static implicit operator Purchasable(Free free) => new(free); + public static implicit operator Purchasable(Packaged packaged) => new(packaged); + public static implicit operator Purchasable(Scalable scalable) => new(scalable); + + public T? FromFree(Func select, Func? fallback = null) => + IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; + + public T? FromPackaged(Func select, Func? fallback = null) => + IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; + + public T? FromScalable(Func select, Func? fallback = null) => + IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default; + + public bool IsFree => IsT0; + public bool IsPackaged => IsT1; + public bool IsScalable => IsT2; +} + +internal class PurchasableJsonConverter : JsonConverter +{ + private static readonly string _typePropertyName = nameof(Free.Type).ToLower(); + + public override Purchasable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var element = JsonElement.ParseValue(ref reader); + + if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty)) + { + throw new JsonException( + $"Failed to deserialize {nameof(Purchasable)}: missing '{_typePropertyName}' property"); + } + + var type = typeProperty.GetString(); + + return type switch + { + "free" => element.Deserialize(options)!, + "packaged" => element.Deserialize(options)!, + "scalable" => element.Deserialize(options)!, + _ => throw new JsonException($"Failed to deserialize {nameof(Purchasable)}: invalid '{_typePropertyName}' value - '{type}'"), + }; + } + + public override void Write(Utf8JsonWriter writer, Purchasable value, JsonSerializerOptions options) + => value.Switch( + free => JsonSerializer.Serialize(writer, free, options), + packaged => JsonSerializer.Serialize(writer, packaged, options), + scalable => JsonSerializer.Serialize(writer, scalable, options) + ); +} + +[JsonConverter(typeof(FreeOrScalableJsonConverter))] +public class FreeOrScalable(OneOf input) : OneOfBase(input) +{ + public static implicit operator FreeOrScalable(Free free) => new(free); + public static implicit operator FreeOrScalable(Scalable scalable) => new(scalable); + + public T? FromFree(Func select, Func? fallback = null) => + IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; + + public T? FromScalable(Func select, Func? fallback = null) => + IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; + + public bool IsFree => IsT0; + public bool IsScalable => IsT1; +} + +public class FreeOrScalableJsonConverter : JsonConverter +{ + private static readonly string _typePropertyName = nameof(Free.Type).ToLower(); + + public override FreeOrScalable Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var element = JsonElement.ParseValue(ref reader); + + if (!element.TryGetProperty(options.PropertyNamingPolicy?.ConvertName(_typePropertyName) ?? _typePropertyName, out var typeProperty)) + { + throw new JsonException( + $"Failed to deserialize {nameof(FreeOrScalable)}: missing '{_typePropertyName}' property"); + } + + var type = typeProperty.GetString(); + + return type switch + { + "free" => element.Deserialize(options)!, + "scalable" => element.Deserialize(options)!, + _ => throw new JsonException($"Failed to deserialize {nameof(FreeOrScalable)}: invalid '{_typePropertyName}' value - '{type}'"), + }; + } + + public override void Write(Utf8JsonWriter writer, FreeOrScalable value, JsonSerializerOptions options) + => value.Switch( + free => JsonSerializer.Serialize(writer, free, options), + scalable => JsonSerializer.Serialize(writer, scalable, options) + ); +} + +public class Free +{ + public int Quantity { get; set; } + public string Type => "free"; +} + +public class Packaged +{ + public int Quantity { get; set; } + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + public AdditionalSeats? Additional { get; set; } + public string Type => "packaged"; + + public class AdditionalSeats + { + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + } +} + +public class Scalable +{ + public int Provided { get; set; } + public string StripePriceId { get; set; } = null!; + public decimal Price { get; set; } + public string Type => "scalable"; +} diff --git a/src/Core/Billing/Pricing/Models/PurchasableDTO.cs b/src/Core/Billing/Pricing/Models/PurchasableDTO.cs deleted file mode 100644 index 8ba1c7b731..0000000000 --- a/src/Core/Billing/Pricing/Models/PurchasableDTO.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System.Text.Json.Serialization; -using Bit.Core.Billing.Pricing.JSON; -using OneOf; - -namespace Bit.Core.Billing.Pricing.Models; - -#nullable enable - -[JsonConverter(typeof(PurchasableDTOJsonConverter))] -public class PurchasableDTO(OneOf input) : OneOfBase(input) -{ - public static implicit operator PurchasableDTO(FreeDTO free) => new(free); - public static implicit operator PurchasableDTO(PackagedDTO packaged) => new(packaged); - public static implicit operator PurchasableDTO(ScalableDTO scalable) => new(scalable); - - public T? FromFree(Func select, Func? fallback = null) => - IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; - - public T? FromPackaged(Func select, Func? fallback = null) => - IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; - - public T? FromScalable(Func select, Func? fallback = null) => - IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default; - - public bool IsFree => IsT0; - public bool IsPackaged => IsT1; - public bool IsScalable => IsT2; -} - -[JsonConverter(typeof(FreeOrScalableDTOJsonConverter))] -public class FreeOrScalableDTO(OneOf input) : OneOfBase(input) -{ - public static implicit operator FreeOrScalableDTO(FreeDTO freeDTO) => new(freeDTO); - public static implicit operator FreeOrScalableDTO(ScalableDTO scalableDTO) => new(scalableDTO); - - public T? FromFree(Func select, Func? fallback = null) => - IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default; - - public T? FromScalable(Func select, Func? fallback = null) => - IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default; - - public bool IsFree => IsT0; - public bool IsScalable => IsT1; -} - -public class FreeDTO -{ - public int Quantity { get; set; } - public string Type => "free"; -} - -public class PackagedDTO -{ - public int Quantity { get; set; } - public string StripePriceId { get; set; } = null!; - public decimal Price { get; set; } - public AdditionalSeats? Additional { get; set; } - public string Type => "packaged"; - - public class AdditionalSeats - { - public string StripePriceId { get; set; } = null!; - public decimal Price { get; set; } - } -} - -public class ScalableDTO -{ - public int Provided { get; set; } - public string StripePriceId { get; set; } = null!; - public decimal Price { get; set; } - public string Type => "scalable"; -} diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/PlanAdapter.cs index 45a48c3f80..560987b891 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/PlanAdapter.cs @@ -1,14 +1,12 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing.Models; -using Bit.Core.Models.StaticStore; - -#nullable enable +using Plan = Bit.Core.Billing.Pricing.Models.Plan; namespace Bit.Core.Billing.Pricing; -public record PlanAdapter : Plan +public record PlanAdapter : Core.Models.StaticStore.Plan { - public PlanAdapter(PlanDTO plan) + public PlanAdapter(Plan plan) { Type = ToPlanType(plan.LookupKey); ProductTier = ToProductTierType(Type); @@ -88,7 +86,7 @@ public record PlanAdapter : Plan _ => throw new BillingException() // TODO: Flesh out }; - private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanDTO plan) + private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(Plan plan) { var stripePlanId = GetStripePlanId(plan.Seats); var stripeSeatPlanId = GetStripeSeatPlanId(plan.Seats); @@ -128,7 +126,7 @@ public record PlanAdapter : Plan }; } - private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanDTO plan) + private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(Plan plan) { var seats = plan.SecretsManager!.Seats; var serviceAccounts = plan.SecretsManager.ServiceAccounts; @@ -165,62 +163,62 @@ public record PlanAdapter : Plan }; } - private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable) + private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalable freeOrScalable) => freeOrScalable.FromScalable(x => x.Price); - private static decimal GetBasePrice(PurchasableDTO purchasable) + private static decimal GetBasePrice(Purchasable purchasable) => purchasable.FromPackaged(x => x.Price); - private static int GetBaseSeats(FreeOrScalableDTO freeOrScalable) + private static int GetBaseSeats(FreeOrScalable freeOrScalable) => freeOrScalable.Match( free => free.Quantity, scalable => scalable.Provided); - private static int GetBaseSeats(PurchasableDTO purchasable) + private static int GetBaseSeats(Purchasable purchasable) => purchasable.Match( free => free.Quantity, packaged => packaged.Quantity, scalable => scalable.Provided); - private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable) + private static short GetBaseServiceAccount(FreeOrScalable freeOrScalable) => freeOrScalable.Match( free => (short)free.Quantity, scalable => (short)scalable.Provided); - private static short? GetMaxSeats(PurchasableDTO purchasable) + private static short? GetMaxSeats(Purchasable purchasable) => purchasable.Match( free => (short)free.Quantity, packaged => (short)packaged.Quantity, _ => null); - private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable) + private static short? GetMaxSeats(FreeOrScalable freeOrScalable) => freeOrScalable.FromFree(x => (short)x.Quantity); - private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable) + private static short? GetMaxServiceAccounts(FreeOrScalable freeOrScalable) => freeOrScalable.FromFree(x => (short)x.Quantity); - private static decimal GetSeatPrice(PurchasableDTO purchasable) + private static decimal GetSeatPrice(Purchasable purchasable) => purchasable.Match( _ => 0, packaged => packaged.Additional?.Price ?? 0, scalable => scalable.Price); - private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable) + private static decimal GetSeatPrice(FreeOrScalable freeOrScalable) => freeOrScalable.FromScalable(x => x.Price); - private static string? GetStripePlanId(PurchasableDTO purchasable) + private static string? GetStripePlanId(Purchasable purchasable) => purchasable.FromPackaged(x => x.StripePriceId); - private static string? GetStripeSeatPlanId(PurchasableDTO purchasable) + private static string? GetStripeSeatPlanId(Purchasable purchasable) => purchasable.Match( _ => null, packaged => packaged.Additional?.StripePriceId, scalable => scalable.StripePriceId); - private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable) + private static string? GetStripeSeatPlanId(FreeOrScalable freeOrScalable) => freeOrScalable.FromScalable(x => x.StripePriceId); - private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable) + private static string? GetStripeServiceAccountPlanId(FreeOrScalable freeOrScalable) => freeOrScalable.FromScalable(x => x.StripePriceId); #endregion diff --git a/src/Core/Billing/Pricing/PricingClient.cs b/src/Core/Billing/Pricing/PricingClient.cs index 14caa54eb4..a3db8ce07f 100644 --- a/src/Core/Billing/Pricing/PricingClient.cs +++ b/src/Core/Billing/Pricing/PricingClient.cs @@ -1,7 +1,6 @@ using System.Net; using System.Net.Http.Json; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing.Models; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; @@ -45,7 +44,7 @@ public class PricingClient( if (response.IsSuccessStatusCode) { - var plan = await response.Content.ReadFromJsonAsync(); + var plan = await response.Content.ReadFromJsonAsync(); if (plan == null) { throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); @@ -93,7 +92,7 @@ public class PricingClient( if (response.IsSuccessStatusCode) { - var plans = await response.Content.ReadFromJsonAsync>(); + var plans = await response.Content.ReadFromJsonAsync>(); if (plans == null) { throw new BillingException(message: "Deserialization of Pricing Service response resulted in null"); diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index ef43bde010..5f656b2c22 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -36,6 +36,9 @@ public interface ISubscriberService ISubscriber subscriber, string paymentMethodNonce); + Task CreateStripeCustomer( + ISubscriber subscriber); + /// /// Retrieves a Stripe using the 's property. /// diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 7a0e78a6dc..73696846ac 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; @@ -13,6 +14,7 @@ using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -27,14 +29,19 @@ using Subscription = Stripe.Subscription; namespace Bit.Core.Billing.Services.Implementations; +using static StripeConstants; + public class SubscriberService( IBraintreeGateway braintreeGateway, IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ITaxService taxService) : ISubscriberService + ITaxService taxService, + IUserRepository userRepository) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -146,6 +153,110 @@ public class SubscriberService( throw new BillingException(); } +#nullable enable + public async Task CreateStripeCustomer(ISubscriber subscriber) + { + if (!string.IsNullOrEmpty(subscriber.GatewayCustomerId)) + { + throw new ConflictException("Subscriber already has a linked Stripe Customer"); + } + + var options = subscriber switch + { + Organization organization => new CustomerCreateOptions + { + Description = organization.DisplayBusinessName(), + Email = organization.BillingEmail, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = organization.SubscriberType(), + Value = Max30Characters(organization.DisplayName()) + } + ] + }, + Metadata = new Dictionary + { + [MetadataKeys.OrganizationId] = organization.Id.ToString(), + [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion + } + }, + Provider provider => new CustomerCreateOptions + { + Description = provider.DisplayBusinessName(), + Email = provider.BillingEmail, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = provider.SubscriberType(), + Value = Max30Characters(provider.DisplayName()) + } + ] + }, + Metadata = new Dictionary + { + [MetadataKeys.ProviderId] = provider.Id.ToString(), + [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion + } + }, + User user => new CustomerCreateOptions + { + Description = user.Name, + Email = user.Email, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = user.SubscriberType(), + Value = Max30Characters(user.SubscriberName()) + } + ] + }, + Metadata = new Dictionary + { + [MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion, + [MetadataKeys.UserId] = user.Id.ToString() + } + }, + _ => throw new ArgumentOutOfRangeException(nameof(subscriber)) + }; + + var customer = await stripeAdapter.CustomerCreateAsync(options); + + switch (subscriber) + { + case Organization organization: + organization.Gateway = GatewayType.Stripe; + organization.GatewayCustomerId = customer.Id; + await organizationRepository.ReplaceAsync(organization); + break; + case Provider provider: + provider.Gateway = GatewayType.Stripe; + provider.GatewayCustomerId = customer.Id; + await providerRepository.ReplaceAsync(provider); + break; + case User user: + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + await userRepository.ReplaceAsync(user); + break; + } + + return customer; + + string? Max30Characters(string? input) + => input?.Length <= 30 ? input : input?[..30]; + } +#nullable disable + public async Task GetCustomer( ISubscriber subscriber, CustomerGetOptions customerGetOptions = null) diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs index 86f233232f..6e061293c7 100644 --- a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs +++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Commands; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; @@ -20,8 +19,11 @@ public class PreviewTaxAmountCommand( ILogger logger, IPricingClient pricingClient, IStripeAdapter stripeAdapter, - ITaxService taxService) : BillingCommand(logger), IPreviewTaxAmountCommand + ITaxService taxService) : BaseBillingCommand(logger), IPreviewTaxAmountCommand { + protected override Conflict DefaultConflict + => new("We had a problem calculating your tax obligation. Please contact support for assistance."); + public Task> Run(OrganizationTrialParameters parameters) => HandleAsync(async () => { diff --git a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs index 453d0c78e9..c42049d5bb 100644 --- a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs +++ b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Services; using Bit.Core.Services; using Bit.Core.Test.Billing.Extensions; using Microsoft.Extensions.Logging; @@ -16,14 +17,15 @@ using static StripeConstants; public class UpdateBillingAddressCommandTests { - private readonly IStripeAdapter _stripeAdapter; + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); private readonly UpdateBillingAddressCommand _command; public UpdateBillingAddressCommandTests() { - _stripeAdapter = Substitute.For(); _command = new UpdateBillingAddressCommand( Substitute.For>(), + _subscriberService, _stripeAdapter); } @@ -86,6 +88,66 @@ public class UpdateBillingAddressCommandTests Arg.Is(options => options.AutomaticTax.Enabled == true)); } + [Fact] + public async Task Run_PersonalOrganization_NoCurrentCustomer_MakesCorrectInvocations_ReturnsBillingAddress() + { + var organization = new Organization + { + PlanType = PlanType.FamiliesAnnually, + GatewaySubscriptionId = "sub_123" + }; + + var input = new BillingAddress + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }; + + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Suite 100", + City = "New York", + State = "NY" + }, + Subscriptions = new StripeList + { + Data = + [ + new Subscription + { + Id = organization.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }; + + _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Address.Matches(input) && + options.HasExpansions("subscriptions") + )).Returns(customer); + + var result = await _command.Run(organization, input); + + Assert.True(result.IsT0); + var output = result.AsT0; + Assert.Equivalent(input, output); + + await _subscriberService.Received(1).CreateStripeCustomer(organization); + + await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + [Fact] public async Task Run_BusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress() { diff --git a/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs index e7bc5c787c..8b1f915658 100644 --- a/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs +++ b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs @@ -45,7 +45,8 @@ public class UpdatePaymentMethodCommandTests { var organization = new Organization { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" }; var customer = new Customer @@ -100,13 +101,75 @@ public class UpdatePaymentMethodCommandTests } [Fact] - public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount() + public async Task Run_BankAccount_NoCurrentCustomer_MakesCorrectInvocations_ReturnsMaskedBankAccount() { var organization = new Organization { Id = Guid.NewGuid() }; + var customer = new Customer + { + Address = new Address + { + Country = "US", + PostalCode = "12345" + }, + Metadata = new Dictionary() + }; + + _subscriberService.GetCustomer(organization).Returns(customer); + + const string token = "TOKEN"; + + var setupIntent = new SetupIntent + { + Id = "seti_123", + PaymentMethod = + new PaymentMethod + { + Type = "us_bank_account", + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + }, + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + Status = "requires_action" + }; + + _stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]); + + var result = await _command.Run(organization, + new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress + { + Country = "US", + PostalCode = "12345" + }); + + Assert.True(result.IsT0); + var maskedPaymentMethod = result.AsT0; + Assert.True(maskedPaymentMethod.IsT0); + var maskedBankAccount = maskedPaymentMethod.AsT0; + Assert.Equal("Chase", maskedBankAccount.BankName); + Assert.Equal("9999", maskedBankAccount.Last4); + Assert.False(maskedBankAccount.Verified); + + await _subscriberService.Received(1).CreateStripeCustomer(organization); + + await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id); + } + + [Fact] + public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" + }; + var customer = new Customer { Address = new Address @@ -170,7 +233,8 @@ public class UpdatePaymentMethodCommandTests { var organization = new Organization { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" }; var customer = new Customer @@ -227,7 +291,8 @@ public class UpdatePaymentMethodCommandTests { var organization = new Organization { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" }; var customer = new Customer @@ -282,7 +347,8 @@ public class UpdatePaymentMethodCommandTests { var organization = new Organization { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" }; var customer = new Customer @@ -343,7 +409,8 @@ public class UpdatePaymentMethodCommandTests { var organization = new Organization { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + GatewayCustomerId = "cus_123" }; var customer = new Customer diff --git a/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs index 345f2dfab8..39753857d5 100644 --- a/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs +++ b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs @@ -25,6 +25,27 @@ public class MaskedPaymentMethodTests Assert.Equivalent(input.AsT0, output.AsT0); } + [Fact] + public void Write_Read_BankAccount_WithOptions_Succeeds() + { + MaskedPaymentMethod input = new MaskedBankAccount + { + BankName = "Chase", + Last4 = "9999", + Verified = true + }; + + var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + var json = JsonSerializer.Serialize(input, jsonSerializerOptions); + + var output = JsonSerializer.Deserialize(json, jsonSerializerOptions); + Assert.NotNull(output); + Assert.True(output.IsT0); + + Assert.Equivalent(input.AsT0, output.AsT0); + } + [Fact] public void Write_Read_Card_Succeeds() { diff --git a/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs index 4d82b4b5c9..8a4475268d 100644 --- a/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs +++ b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Test.Billing.Extensions; using Braintree; using Microsoft.Extensions.Logging; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; using Customer = Stripe.Customer; @@ -35,6 +36,23 @@ public class GetPaymentMethodQueryTests _subscriberService); } + [Fact] + public async Task Run_NoCustomer_ReturnsNull() + { + var organization = new Organization + { + Id = Guid.NewGuid() + }; + + _subscriberService.GetCustomer(organization, + Arg.Is(options => + options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).ReturnsNull(); + + var maskedPaymentMethod = await _query.Run(organization); + + Assert.Null(maskedPaymentMethod); + } + [Fact] public async Task Run_NoPaymentMethod_ReturnsNull() { From 676f39cef8170767c2b74fc01fb44d2d3394fa92 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 14 Jul 2025 15:50:10 -0400 Subject: [PATCH 053/326] [PM-20554] fix admin endpoint for deleting unassigned items (#6061) * fix admin endpoint for deleting unassigned items * whitespace cleanup * fix tests * switch type cast to constructor for CipherDetails * fix tests --- .../Vault/Controllers/CiphersController.cs | 13 +- .../Controllers/CiphersControllerTests.cs | 183 ++++++++++-------- 2 files changed, 107 insertions(+), 89 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 853dadebd0..98ec78e9a0 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -926,14 +926,14 @@ public class CiphersController : Controller public async Task PutDeleteAdmin(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var cipher = await GetByIdAsyncAdmin(id); if (cipher == null || !cipher.OrganizationId.HasValue || !await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) { throw new NotFoundException(); } - await _cipherService.SoftDeleteAsync(cipher, userId, true); + await _cipherService.SoftDeleteAsync(new CipherDetails(cipher), userId, true); } [HttpPut("delete")] @@ -995,14 +995,14 @@ public class CiphersController : Controller public async Task PutRestoreAdmin(Guid id) { var userId = _userService.GetProperUserId(User).Value; - var cipher = await GetByIdAsync(id, userId); + var cipher = await GetByIdAsyncAdmin(id); if (cipher == null || !cipher.OrganizationId.HasValue || !await CanDeleteOrRestoreCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) { throw new NotFoundException(); } - await _cipherService.RestoreAsync(cipher, userId, true); + await _cipherService.RestoreAsync(new CipherDetails(cipher), userId, true); return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); } @@ -1412,6 +1412,11 @@ public class CiphersController : Controller } } + private async Task GetByIdAsyncAdmin(Guid cipherId) + { + return await _cipherRepository.GetOrganizationDetailsByIdAsync(cipherId); + } + private async Task GetByIdAsync(Guid cipherId, Guid userId) { return await _cipherRepository.GetByIdAsync(cipherId, userId); diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index d1f5a212c9..2819fb8880 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -590,11 +590,13 @@ public class CiphersControllerTests [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithManagePermission_SoftDeletesCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.UserId = null; - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.UserId = null; + cipherOrgDetails.OrganizationId = organization.Id; + + var cipherDetails = new CipherDetails(cipherOrgDetails); cipherDetails.Edit = true; cipherDetails.Manage = true; @@ -603,7 +605,7 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) .Returns(new List @@ -620,7 +622,8 @@ public class CiphersControllerTests await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).SoftDeleteAsync( + Arg.Is(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] @@ -665,20 +668,20 @@ public class CiphersControllerTests [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_SoftDeletesCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.OrganizationId = organization.Id; organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency() .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) - .Returns(new List { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } }); + .Returns(new List { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } }); sutProvider.GetDependency() .GetOrganizationAbilityAsync(organization.Id) .Returns(new OrganizationAbility @@ -687,74 +690,80 @@ public class CiphersControllerTests LimitItemDeletion = true }); - await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); + await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id); - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).SoftDeleteAsync( + Arg.Is(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_SoftDeletesCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.OrganizationId = organization.Id; organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherOrgDetails }); sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility { Id = organization.Id, AllowAdminAccessToAllCollectionItems = true }); - await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); + await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id); - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).SoftDeleteAsync( + Arg.Is(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData] public async Task PutDeleteAdmin_WithCustomUser_WithEditAnyCollectionTrue_SoftDeletesCipher( - CipherDetails cipherDetails, Guid userId, + CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.OrganizationId = organization.Id; organization.Type = OrganizationUserType.Custom; organization.Permissions.EditAnyCollection = true; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherOrgDetails }); - await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); + await sutProvider.Sut.PutDeleteAdmin(cipherOrgDetails.Id); - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).SoftDeleteAsync( + Arg.Is(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutDeleteAdmin_WithOwnerOrAdmin_WithEditPermission_WithLimitItemDeletionFalse_SoftDeletesCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.UserId = null; - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.UserId = null; + cipherOrgDetails.OrganizationId = organization.Id; + + var cipherDetails = new CipherDetails(cipherOrgDetails); cipherDetails.Edit = true; cipherDetails.Manage = false; // Only Edit permission, not Manage + organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) .Returns(new List { cipherDetails }); @@ -768,7 +777,8 @@ public class CiphersControllerTests await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).SoftDeleteAsync( + Arg.Is(c => c.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] @@ -787,7 +797,7 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) .Returns(new List { cipherDetails }); @@ -1061,13 +1071,15 @@ public class CiphersControllerTests [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithManagePermission_RestoresCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.UserId = null; - cipherDetails.OrganizationId = organization.Id; - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + cipherOrgDetails.UserId = null; + cipherOrgDetails.OrganizationId = organization.Id; + cipherOrgDetails.Type = CipherType.Login; + cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + + var cipherDetails = new CipherDetails(cipherOrgDetails); cipherDetails.Edit = true; cipherDetails.Manage = true; @@ -1076,13 +1088,10 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) - .Returns(new List - { - cipherDetails - }); + .Returns(new List { cipherDetails }); sutProvider.GetDependency() .GetOrganizationAbilityAsync(organization.Id) .Returns(new OrganizationAbility @@ -1091,21 +1100,24 @@ public class CiphersControllerTests LimitItemDeletion = true }); - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); + var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( + (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithoutManagePermission_ThrowsNotFoundException( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.UserId = null; - cipherDetails.OrganizationId = organization.Id; + cipherOrgDetails.UserId = null; + cipherOrgDetails.OrganizationId = organization.Id; + + var cipherDetails = new CipherDetails(cipherOrgDetails); cipherDetails.Edit = true; cipherDetails.Manage = false; @@ -1114,13 +1126,10 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) - .Returns(new List - { - cipherDetails - }); + .Returns(new List { cipherDetails }); sutProvider.GetDependency() .GetOrganizationAbilityAsync(organization.Id) .Returns(new OrganizationAbility @@ -1136,21 +1145,22 @@ public class CiphersControllerTests [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToUnassignedCipher_RestoresCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + cipherOrgDetails.OrganizationId = organization.Id; + cipherOrgDetails.Type = CipherType.Login; + cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); sutProvider.GetDependency() .GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organization.Id) - .Returns(new List { new() { Id = cipherDetails.Id, OrganizationId = organization.Id } }); + .Returns(new List { new() { Id = cipherOrgDetails.Id, OrganizationId = organization.Id } }); sutProvider.GetDependency() .GetOrganizationAbilityAsync(organization.Id) .Returns(new OrganizationAbility @@ -1159,82 +1169,88 @@ public class CiphersControllerTests LimitItemDeletion = true }); - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); + var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( + (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithAccessToAllCollectionItems_RestoresCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + cipherOrgDetails.OrganizationId = organization.Id; + cipherOrgDetails.Type = CipherType.Login; + cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherOrgDetails }); sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility { Id = organization.Id, AllowAdminAccessToAllCollectionItems = true }); - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); + var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( + (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData] public async Task PutRestoreAdmin_WithCustomUser_WithEditAnyCollectionTrue_RestoresCipher( - CipherDetails cipherDetails, Guid userId, + CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.OrganizationId = organization.Id; - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + cipherOrgDetails.OrganizationId = organization.Id; + cipherOrgDetails.Type = CipherType.Login; + cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); organization.Type = OrganizationUserType.Custom; organization.Permissions.EditAnyCollection = true; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherOrgDetails.Id).Returns(cipherOrgDetails); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherOrgDetails }); - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); + var result = await sutProvider.Sut.PutRestoreAdmin(cipherOrgDetails.Id); Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( + (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] public async Task PutRestoreAdmin_WithOwnerOrAdmin_WithEditPermission_LimitItemDeletionFalse_RestoresCipher( - OrganizationUserType organizationUserType, CipherDetails cipherDetails, Guid userId, + OrganizationUserType organizationUserType, CipherOrganizationDetails cipherOrgDetails, Guid userId, CurrentContextOrganization organization, SutProvider sutProvider) { - cipherDetails.UserId = null; - cipherDetails.OrganizationId = organization.Id; - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + cipherOrgDetails.UserId = null; + cipherOrgDetails.OrganizationId = organization.Id; + cipherOrgDetails.Type = CipherType.Login; + cipherOrgDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); + + var cipherDetails = new CipherDetails(cipherOrgDetails); cipherDetails.Edit = true; cipherDetails.Manage = false; // Only Edit permission, not Manage + organization.Type = organizationUserType; sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) .Returns(new List { cipherDetails }); @@ -1249,7 +1265,8 @@ public class CiphersControllerTests var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); + await sutProvider.GetDependency().Received(1).RestoreAsync(Arg.Is( + (cd) => cd.OrganizationId.Equals(cipherOrgDetails.OrganizationId)), userId, true); } [Theory] @@ -1270,7 +1287,7 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId }); sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); + sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); sutProvider.GetDependency() .GetManyByUserIdAsync(userId) .Returns(new List { cipherDetails }); @@ -1319,10 +1336,6 @@ public class CiphersControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id)); } - - - - [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] From 93a00373d2c55974dfeb196af4f54b9ed64eb26f Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 15 Jul 2025 07:38:14 -0400 Subject: [PATCH 054/326] Add feature flag for using sdk password generators (#6082) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2ef9a20ae3..ad726a40fe 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -199,6 +199,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"; /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; From c4965350d19306e206bf99df3726a841d1d11407 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 15 Jul 2025 07:52:47 -0500 Subject: [PATCH 055/326] [PM-12474] Move to authorization to attibutes/handlers/requirements (#6001) * Created ReadAllOrganizationUsersBasicInformationRequirement for use with Authorize attribute. * Removed unused req and Handler and tests. Moved to new auth attribute * Moved tests to integration tests with new response. * Removed tests that were migrated to integration tests. * Made string params Guids instead of parsing them manually in methods. * Admin and Owner added to requirement. * Added XML docs for basic get endpoint. Removed unused. Added another auth check. Inverted if check. * Removed unused endpoint * Added tests for requirement * Added checks for both User and Custom * Added org id check to validate the user being requested belongs to the org in the route. * typo --- .../ManageGroupsOrUsersRequirement.cs | 17 ++ .../OrganizationUsersController.cs | 190 +++++------------- ...UserUserMiniDetailsAuthorizationHandler.cs | 50 ----- ...OrganizationServiceCollectionExtensions.cs | 1 - .../OrganizationUserControllerTests.cs | 101 ++++++++++ .../ManageGroupsOrUsersRequirementTests.cs | 84 ++++++++ .../OrganizationUsersControllerTests.cs | 36 +--- ...serMiniDetailsAuthorizationHandlerTests.cs | 81 -------- 8 files changed, 253 insertions(+), 307 deletions(-) create mode 100644 src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs create mode 100644 test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs create mode 100644 test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs delete mode 100644 test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs new file mode 100644 index 0000000000..55dfb766d6 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs @@ -0,0 +1,17 @@ +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class ManageGroupsOrUsersRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync(CurrentContextOrganization organizationClaims, Func> isProviderUserForOrg) => + organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Permissions.ManageGroups: true } => true, + { Permissions.ManageUsers: true } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 81c31355e3..5409adc825 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Api.AdminConsole.Authorization; using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; @@ -126,10 +127,11 @@ public class OrganizationUsersController : Controller } [HttpGet("{id}")] - public async Task Get(Guid id, bool includeGroups = false) + [Authorize] + public async Task Get(Guid orgId, Guid id, bool includeGroups = false) { var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); - if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId)) + if (organizationUser == null || organizationUser.OrganizationId != orgId) { throw new NotFoundException(); } @@ -148,16 +150,17 @@ public class OrganizationUsersController : Controller return response; } + /// + /// Returns a set of basic information about all members of the organization. This is available to all members of + /// the organization to manage collections. For this reason, it contains as little information as possible and no + /// cryptographic keys or other sensitive data. + /// + /// Organization identifier + /// List of users for the organization. [HttpGet("mini-details")] + [Authorize] public async Task> GetMiniDetails(Guid orgId) { - var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), - OrganizationUserUserMiniDetailsOperations.ReadAll); - if (!authorizationResult.Succeeded) - { - throw new NotFoundException(); - } - var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId); return new ListResponseModel( organizationUserUserDetails.Select(ou => new OrganizationUserUserMiniDetailsResponseModel(ou))); @@ -207,7 +210,7 @@ public class OrganizationUsersController : Controller { OrganizationId = orgId, IncludeGroups = includeGroups, - IncludeCollections = includeCollections, + IncludeCollections = includeCollections }; if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded) @@ -231,34 +234,12 @@ public class OrganizationUsersController : Controller .ToList()); } - - [HttpGet("{id}/groups")] - public async Task> GetGroups(string orgId, string id) - { - var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (organizationUser == null || (!await _currentContext.ManageGroups(organizationUser.OrganizationId) && - !await _currentContext.ManageUsers(organizationUser.OrganizationId))) - { - throw new NotFoundException(); - } - - var groupIds = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id); - var responses = groupIds.Select(g => g.ToString()); - return responses; - } - [HttpGet("{id}/reset-password-details")] - public async Task GetResetPasswordDetails(string orgId, string id) + [Authorize] + public async Task GetResetPasswordDetails(Guid orgId, Guid id) { - // Make sure the calling user can reset passwords for this org - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageResetPassword(orgGuidId)) - { - throw new NotFoundException(); - } - - var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); - if (organizationUser == null || !organizationUser.UserId.HasValue) + var organizationUser = await _organizationUserRepository.GetByIdAsync(id); + if (organizationUser is null || organizationUser.UserId is null) { throw new NotFoundException(); } @@ -272,7 +253,7 @@ public class OrganizationUsersController : Controller } // Retrieve Encrypted Private Key from organization - var org = await _organizationRepository.GetByIdAsync(orgGuidId); + var org = await _organizationRepository.GetByIdAsync(orgId); if (org == null) { throw new NotFoundException(); @@ -282,26 +263,17 @@ public class OrganizationUsersController : Controller } [HttpPost("account-recovery-details")] + [Authorize] public async Task> GetAccountRecoveryDetails(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - // Make sure the calling user can reset passwords for this org - if (!await _currentContext.ManageResetPassword(orgId)) - { - throw new NotFoundException(); - } - var responses = await _organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync(orgId, model.Ids); return new ListResponseModel(responses.Select(r => new OrganizationUserResetPasswordDetailsResponseModel(r))); } [HttpPost("invite")] + [Authorize] public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - // Check the user has permission to grant access to the collections for the new user if (model.Collections?.Any() == true) { @@ -317,35 +289,25 @@ public class OrganizationUsersController : Controller var userId = _userService.GetProperUserId(User); await _organizationService.InviteUsersAsync(orgId, userId.Value, systemUser: null, - new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) }); + [(new OrganizationUserInvite(model.ToData()), null)]); } [HttpPost("reinvite")] - public async Task> BulkReinvite(string orgId, [FromBody] OrganizationUserBulkRequestModel model) + [Authorize] + public async Task> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); - var result = await _organizationService.ResendInvitesAsync(orgGuidId, userId.Value, model.Ids); + var result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids); return new ListResponseModel( result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2))); } [HttpPost("{id}/reinvite")] - public async Task Reinvite(string orgId, string id) + [Authorize] + public async Task Reinvite(Guid orgId, Guid id) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); - await _organizationService.ResendInviteAsync(orgGuidId, userId.Value, new Guid(id)); + await _organizationService.ResendInviteAsync(orgId, userId.Value, id); } [HttpPost("{organizationUserId}/accept-init")] @@ -406,57 +368,39 @@ public class OrganizationUsersController : Controller } [HttpPost("{id}/confirm")] + [Authorize] public async Task Confirm(Guid orgId, Guid id, [FromBody] OrganizationUserConfirmRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); - var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName); + _ = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName); } [HttpPost("confirm")] - public async Task> BulkConfirm(string orgId, + [Authorize] + public async Task> BulkConfirm(Guid orgId, [FromBody] OrganizationUserBulkConfirmRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); - var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value); + var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgId, model.ToDictionary(), userId.Value); return new ListResponseModel(results.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); } [HttpPost("public-keys")] - public async Task> UserPublicKeys(string orgId, [FromBody] OrganizationUserBulkRequestModel model) + [Authorize] + public async Task> UserPublicKeys(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - var orgGuidId = new Guid(orgId); - if (!await _currentContext.ManageUsers(orgGuidId)) - { - throw new NotFoundException(); - } - - var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgGuidId, model.Ids); + var result = await _organizationUserRepository.GetManyPublicKeysByOrganizationUserAsync(orgId, model.Ids); var responses = result.Select(r => new OrganizationUserPublicKeyResponseModel(r.Id, r.UserId, r.PublicKey)).ToList(); return new ListResponseModel(responses); } [HttpPut("{id}")] [HttpPost("{id}")] + [Authorize] public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var (organizationUser, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id); if (organizationUser == null || organizationUser.OrganizationId != orgId) { @@ -557,27 +501,19 @@ public class OrganizationUsersController : Controller } [HttpPut("{id}/reset-password")] - public async Task PutResetPassword(string orgId, string id, [FromBody] OrganizationUserResetPasswordRequestModel model) + [Authorize] + public async Task PutResetPassword(Guid orgId, Guid id, [FromBody] OrganizationUserResetPasswordRequestModel model) { - - var orgGuidId = new Guid(orgId); - - // Calling user must have Manage Reset Password permission - if (!await _currentContext.ManageResetPassword(orgGuidId)) - { - throw new NotFoundException(); - } - // Get the users role, since provider users aren't a member of the organization we use the owner check - var orgUserType = await _currentContext.OrganizationOwner(orgGuidId) + var orgUserType = await _currentContext.OrganizationOwner(orgId) ? OrganizationUserType.Owner - : _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgGuidId)?.Type; + : _currentContext.Organizations?.FirstOrDefault(o => o.Id == orgId)?.Type; if (orgUserType == null) { throw new NotFoundException(); } - var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgGuidId, new Guid(id), model.NewMasterPasswordHash, model.Key); + var result = await _userService.AdminResetPasswordAsync(orgUserType.Value, orgId, id, model.NewMasterPasswordHash, model.Key); if (result.Succeeded) { return; @@ -594,26 +530,18 @@ public class OrganizationUsersController : Controller [HttpDelete("{id}")] [HttpPost("{id}/remove")] + [Authorize] public async Task Remove(Guid orgId, Guid id) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value); } [HttpDelete("")] [HttpPost("remove")] + [Authorize] public async Task> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => @@ -622,13 +550,9 @@ public class OrganizationUsersController : Controller [HttpDelete("{id}/delete-account")] [HttpPost("{id}/delete-account")] + [Authorize] public async Task DeleteAccount(Guid orgId, Guid id) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var currentUser = await _userService.GetUserByPrincipalAsync(User); if (currentUser == null) { @@ -640,13 +564,9 @@ public class OrganizationUsersController : Controller [HttpDelete("delete-account")] [HttpPost("delete-account")] + [Authorize] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var currentUser = await _userService.GetUserByPrincipalAsync(User); if (currentUser == null) { @@ -661,6 +581,7 @@ public class OrganizationUsersController : Controller [HttpPatch("{id}/revoke")] [HttpPut("{id}/revoke")] + [Authorize] public async Task RevokeAsync(Guid orgId, Guid id) { await RestoreOrRevokeUserAsync(orgId, id, _organizationService.RevokeUserAsync); @@ -668,6 +589,7 @@ public class OrganizationUsersController : Controller [HttpPatch("revoke")] [HttpPut("revoke")] + [Authorize] public async Task> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { return await RestoreOrRevokeUsersAsync(orgId, model, _organizationService.RevokeUsersAsync); @@ -675,6 +597,7 @@ public class OrganizationUsersController : Controller [HttpPatch("{id}/restore")] [HttpPut("{id}/restore")] + [Authorize] public async Task RestoreAsync(Guid orgId, Guid id) { await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId)); @@ -682,6 +605,7 @@ public class OrganizationUsersController : Controller [HttpPatch("restore")] [HttpPut("restore")] + [Authorize] public async Task> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService)); @@ -689,14 +613,10 @@ public class OrganizationUsersController : Controller [HttpPatch("enable-secrets-manager")] [HttpPut("enable-secrets-manager")] + [Authorize] public async Task BulkEnableSecretsManagerAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids)) .Where(ou => ou.OrganizationId == orgId && !ou.AccessSecretsManager).ToList(); if (orgUsers.Count == 0) @@ -729,11 +649,6 @@ public class OrganizationUsersController : Controller Guid id, Func statusAction) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); var orgUser = await _organizationUserRepository.GetByIdAsync(id); if (orgUser == null || orgUser.OrganizationId != orgId) @@ -749,11 +664,6 @@ public class OrganizationUsersController : Controller OrganizationUserBulkRequestModel model, Func, Guid?, Task>>> statusAction) { - if (!await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - var userId = _userService.GetProperUserId(User); var result = await statusAction(orgId, model.Ids, userId.Value); return new ListResponseModel(result.Select(r => diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs deleted file mode 100644 index e63b6bf096..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; -using Bit.Core.Context; -using Microsoft.AspNetCore.Authorization; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; - -public class OrganizationUserUserMiniDetailsAuthorizationHandler : - AuthorizationHandler -{ - private readonly ICurrentContext _currentContext; - - public OrganizationUserUserMiniDetailsAuthorizationHandler(ICurrentContext currentContext) - { - _currentContext = currentContext; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, - OrganizationUserUserMiniDetailsOperationRequirement requirement, OrganizationScope organizationScope) - { - var authorized = false; - - switch (requirement) - { - case not null when requirement.Name == nameof(OrganizationUserUserMiniDetailsOperations.ReadAll): - authorized = await CanReadAllAsync(organizationScope); - break; - } - - if (authorized) - { - context.Succeed(requirement); - } - } - - private async Task CanReadAllAsync(Guid organizationId) - { - // All organization users can access this data to manage collection access - var organization = _currentContext.GetOrganization(organizationId); - if (organization != null) - { - return true; - } - - // Providers can also access this to manage the organization generally - return await _currentContext.ProviderUserForOrgAsync(organizationId); - } -} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index ac1fe262c2..ae24017e48 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -178,7 +178,6 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs new file mode 100644 index 0000000000..ca13585017 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -0,0 +1,101 @@ +using System.Net; +using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class OrganizationUserControllerTests : IClassFixture, IAsyncLifetime +{ + public OrganizationUserControllerTests(ApiApplicationFactory apiFactory) + { + _factory = apiFactory; + _client = _factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private Organization _organization = null!; + private string _ownerEmail = null!; + + [Theory] + [InlineData(OrganizationUserType.User)] + [InlineData(OrganizationUserType.Custom)] + public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType) + { + var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, organizationUserType, new Permissions { ManageUsers = false }); + + await _loginHelper.LoginAsync(userEmail); + + var request = new OrganizationUserBulkRequestModel + { + Ids = new List { Guid.NewGuid() } + }; + + var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/remove", request); + + Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode); + } + + [Theory] + [InlineData(OrganizationUserType.User)] + [InlineData(OrganizationUserType.Custom)] + public async Task DeleteAccount_WhenUserCannotManageUsers_ReturnsForbiddenResponse(OrganizationUserType organizationUserType) + { + var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, organizationUserType, new Permissions { ManageUsers = false }); + + await _loginHelper.LoginAsync(userEmail); + + var userToRemove = Guid.NewGuid(); + + var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{userToRemove}"); + + Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode); + } + + [Theory] + [InlineData(OrganizationUserType.User)] + [InlineData(OrganizationUserType.Custom)] + public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_ReturnsForbiddenResponse(OrganizationUserType organizationUserType) + { + var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, organizationUserType, new Permissions { ManageUsers = false }); + + await _loginHelper.LoginAsync(userEmail); + + var request = new OrganizationUserBulkRequestModel + { + Ids = [] + }; + + var httpResponse = + await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/account-recovery-details", request); + + Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, + ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } +} diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs new file mode 100644 index 0000000000..1d6270ba1f --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirementTests.cs @@ -0,0 +1,84 @@ +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Requirements; + +[SutProviderCustomize] +public class ManageGroupsOrUsersRequirementTests +{ + [Theory] + [CurrentContextOrganizationCustomize] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task AuthorizeAsync_WhenUserTypeCanManageUsers_ThenRequestShouldBeAuthorized( + OrganizationUserType type, + CurrentContextOrganization organization, + SutProvider sutProvider) + { + organization.Type = type; + + var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false)); + + Assert.True(actual); + } + + [Theory] + [CurrentContextOrganizationCustomize] + [BitAutoData(OrganizationUserType.Custom, true, false)] + [BitAutoData(OrganizationUserType.Custom, false, true)] + public async Task AuthorizeAsync_WhenCustomUserThatCanManageUsersOrGroups_ThenRequestShouldBeAuthorized( + OrganizationUserType type, + bool canManageUsers, + bool canManageGroups, + CurrentContextOrganization organization, + SutProvider sutProvider) + { + organization.Type = type; + organization.Permissions = new Permissions { ManageUsers = canManageUsers, ManageGroups = canManageGroups }; + + var actual = await sutProvider.Sut.AuthorizeAsync(organization, () => Task.FromResult(false)); + + Assert.True(actual); + } + + [Theory] + [CurrentContextOrganizationCustomize] + [BitAutoData] + public async Task AuthorizeAsync_WhenProviderUserForAnOrganization_ThenRequestShouldBeAuthorized( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsProviderUserForOrg); + + Assert.True(actual); + return; + + Task IsProviderUserForOrg() => Task.FromResult(true); + } + + [Theory] + [CurrentContextOrganizationCustomize] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task AuthorizeAsync_WhenUserCannotManageUsersOrGroupsAndIsNotAProviderUser_ThenRequestShouldBeDenied( + OrganizationUserType type, + CurrentContextOrganization organization, + SutProvider sutProvider) + { + organization.Type = type; + organization.Permissions = new Permissions { ManageUsers = false, ManageGroups = false }; // When Type is User, the canManage permissions don't matter + + var actual = await sutProvider.Sut.AuthorizeAsync(organization, IsNotProviderUserForOrg); + + Assert.False(actual); + return; + + Task IsNotProviderUserForOrg() => Task.FromResult(false); + } +} diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index de54a44bca..cc480d1dcb 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -257,7 +257,7 @@ public class OrganizationUsersControllerTests .GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Is>(ids => ids.Contains(organizationUser.Id))) .Returns(new Dictionary { { organizationUser.Id, true } }); - var response = await sutProvider.Sut.Get(organizationUser.Id, false); + var response = await sutProvider.Sut.Get(organizationUser.OrganizationId, organizationUser.Id, false); Assert.Equal(organizationUser.Id, response.Id); Assert.True(response.ManagedByOrganization); @@ -303,18 +303,6 @@ public class OrganizationUsersControllerTests ou.EncryptedPrivateKey == r.EncryptedPrivateKey))); } - [Theory] - [BitAutoData] - public async Task GetAccountRecoveryDetails_WithoutManageResetPasswordPermission_Throws( - Guid organizationId, - OrganizationUserBulkRequestModel bulkRequestModel, - SutProvider sutProvider) - { - sutProvider.GetDependency().ManageResetPassword(organizationId).Returns(false); - - await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAccountRecoveryDetails(organizationId, bulkRequestModel)); - } - [Theory] [BitAutoData] public async Task DeleteAccount_WhenUserCanManageUsers_Success( @@ -330,17 +318,6 @@ public class OrganizationUsersControllerTests .DeleteUserAsync(orgId, id, currentUser.Id); } - [Theory] - [BitAutoData] - public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( - Guid orgId, Guid id, SutProvider sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(false); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteAccount(orgId, id)); - } - [Theory] [BitAutoData] public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( @@ -374,17 +351,6 @@ public class OrganizationUsersControllerTests .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); } - [Theory] - [BitAutoData] - public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException( - Guid orgId, OrganizationUserBulkRequestModel model, SutProvider sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(false); - - await Assert.ThrowsAsync(() => - sutProvider.Sut.BulkDeleteAccount(orgId, model)); - } - [Theory] [BitAutoData] public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( diff --git a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs b/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs deleted file mode 100644 index 6732486a54..0000000000 --- a/test/Core.Test/AdminConsole/Authorization/OrganizationUserUserMiniDetailsAuthorizationHandlerTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Security.Claims; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; -using Bit.Core.Context; -using Bit.Core.Enums; -using Bit.Core.Test.AdminConsole.AutoFixture; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Authorization; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.AdminConsole.Authorization; - -[SutProviderCustomize] -public class OrganizationUserUserMiniDetailsAuthorizationHandlerTests -{ - [Theory, CurrentContextOrganizationCustomize] - [BitAutoData(OrganizationUserType.Admin)] - [BitAutoData(OrganizationUserType.Owner)] - [BitAutoData(OrganizationUserType.Custom)] - [BitAutoData(OrganizationUserType.User)] - public async Task ReadAll_AnyOrganizationMember_Success( - OrganizationUserType userType, - CurrentContextOrganization organization, - SutProvider sutProvider) - { - organization.Type = userType; - sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); - - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserMiniDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id)); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData, CurrentContextOrganizationCustomize] - public async Task ReadAll_ProviderUser_Success( - CurrentContextOrganization organization, - SutProvider sutProvider) - { - organization.Type = OrganizationUserType.User; - sutProvider.GetDependency() - .GetOrganization(organization.Id) - .Returns((CurrentContextOrganization)null); - sutProvider.GetDependency() - .ProviderUserForOrgAsync(organization.Id) - .Returns(true); - - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserMiniDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id)); - - await sutProvider.Sut.HandleAsync(context); - - Assert.True(context.HasSucceeded); - } - - [Theory, BitAutoData, CurrentContextOrganizationCustomize] - public async Task ReadAll_NotMember_NoSuccess( - CurrentContextOrganization organization, - SutProvider sutProvider) - { - var context = new AuthorizationHandlerContext( - new[] { OrganizationUserUserMiniDetailsOperations.ReadAll }, - new ClaimsPrincipal(), - new OrganizationScope(organization.Id) - ); - - sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); - sutProvider.GetDependency().ProviderUserForOrgAsync(Arg.Any()).Returns(false); - - await sutProvider.Sut.HandleAsync(context); - Assert.False(context.HasSucceeded); - } -} From d3c0dca178f6251240b6ebe0b6c390ab264a19b8 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 15 Jul 2025 08:47:40 -0500 Subject: [PATCH 056/326] fixing method signature. (#6088) --- .../Requirements/ManageGroupsOrUsersRequirement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs index 55dfb766d6..daa5c025cb 100644 --- a/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageGroupsOrUsersRequirement.cs @@ -5,7 +5,7 @@ namespace Bit.Api.AdminConsole.Authorization.Requirements; public class ManageGroupsOrUsersRequirement : IOrganizationRequirement { - public async Task AuthorizeAsync(CurrentContextOrganization organizationClaims, Func> isProviderUserForOrg) => + public async Task AuthorizeAsync(CurrentContextOrganization? organizationClaims, Func> isProviderUserForOrg) => organizationClaims switch { { Type: OrganizationUserType.Owner } => true, From 42ff09b84faea8a9840363daab97c1d0c550bf32 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 15 Jul 2025 15:53:29 +0200 Subject: [PATCH 057/326] [PM-22423] Add MJML (#5941) Scaffolds MJML and adds some initial templates and components. Of interest are: * src/Core/MailTemplates/Mjml/components/hero.js demonstrates how to create a custom MJML component. In our case it's a hero component with our logo, a title, a call to action button and an image. * src/Core/MailTemplates/Mjml/components/head.mjml defines some common styling. * src/Core/MailTemplates/Mjml/components/footer.mjml social links and footer. --- .github/CODEOWNERS | 3 + .gitignore | 1 + src/Core/MailTemplates/Mjml/.mjmlconfig | 5 + src/Core/MailTemplates/Mjml/README.md | 19 + src/Core/MailTemplates/Mjml/build.sh | 4 + .../MailTemplates/Mjml/components/footer.mjml | 53 + .../MailTemplates/Mjml/components/head.mjml | 16 + .../MailTemplates/Mjml/components/hero.js | 64 + .../MailTemplates/Mjml/components/logo.mjml | 11 + .../MailTemplates/Mjml/emails/invite.mjml | 49 + .../MailTemplates/Mjml/emails/two-factor.mjml | 27 + src/Core/MailTemplates/Mjml/package-lock.json | 2186 +++++++++++++++++ src/Core/MailTemplates/Mjml/package.json | 30 + 13 files changed, 2468 insertions(+) create mode 100644 src/Core/MailTemplates/Mjml/.mjmlconfig create mode 100644 src/Core/MailTemplates/Mjml/README.md create mode 100755 src/Core/MailTemplates/Mjml/build.sh create mode 100644 src/Core/MailTemplates/Mjml/components/footer.mjml create mode 100644 src/Core/MailTemplates/Mjml/components/head.mjml create mode 100644 src/Core/MailTemplates/Mjml/components/hero.js create mode 100644 src/Core/MailTemplates/Mjml/components/logo.mjml create mode 100644 src/Core/MailTemplates/Mjml/emails/invite.mjml create mode 100644 src/Core/MailTemplates/Mjml/emails/two-factor.mjml create mode 100644 src/Core/MailTemplates/Mjml/package-lock.json create mode 100644 src/Core/MailTemplates/Mjml/package.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5399bed391..51c5f8c8e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -33,6 +33,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops # Shared util projects util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev +# UIF +src/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project + # Auth team **/Auth @bitwarden/team-auth-dev bitwarden_license/src/Sso @bitwarden/team-auth-dev diff --git a/.gitignore b/.gitignore index 65157bf4aa..e1b2153433 100644 --- a/.gitignore +++ b/.gitignore @@ -214,6 +214,7 @@ bitwarden_license/src/Sso/wwwroot/assets .idea/* **/**.swp .mono +src/Core/MailTemplates/Mjml/out src/Admin/Admin.zip src/Api/Api.zip diff --git a/src/Core/MailTemplates/Mjml/.mjmlconfig b/src/Core/MailTemplates/Mjml/.mjmlconfig new file mode 100644 index 0000000000..7560e0fb96 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/.mjmlconfig @@ -0,0 +1,5 @@ +{ + "packages": [ + "components/hero" + ] +} diff --git a/src/Core/MailTemplates/Mjml/README.md b/src/Core/MailTemplates/Mjml/README.md new file mode 100644 index 0000000000..b60655140a --- /dev/null +++ b/src/Core/MailTemplates/Mjml/README.md @@ -0,0 +1,19 @@ +# Email templates + +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. + +## Usage + +```bash +npm ci + +# Build once +npm run build + +# To build on changes +npm run watch +``` + +## Development + +MJML supports components and you can create your own components by adding them to `.mjmlconfig`. diff --git a/src/Core/MailTemplates/Mjml/build.sh b/src/Core/MailTemplates/Mjml/build.sh new file mode 100755 index 0000000000..c76bdd8f61 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/build.sh @@ -0,0 +1,4 @@ +# 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/footer.mjml b/src/Core/MailTemplates/Mjml/components/footer.mjml new file mode 100644 index 0000000000..0634033618 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/footer.mjml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + +

+ © 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 +

+
+
+
diff --git a/src/Core/MailTemplates/Mjml/components/head.mjml b/src/Core/MailTemplates/Mjml/components/head.mjml new file mode 100644 index 0000000000..929057fb70 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/head.mjml @@ -0,0 +1,16 @@ + + + + + + + + .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; } + diff --git a/src/Core/MailTemplates/Mjml/components/hero.js b/src/Core/MailTemplates/Mjml/components/hero.js new file mode 100644 index 0000000000..6c5bd9bc99 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/hero.js @@ -0,0 +1,64 @@ +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/logo.mjml b/src/Core/MailTemplates/Mjml/components/logo.mjml new file mode 100644 index 0000000000..b8e46a1137 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/components/logo.mjml @@ -0,0 +1,11 @@ + + + + + diff --git a/src/Core/MailTemplates/Mjml/emails/invite.mjml b/src/Core/MailTemplates/Mjml/emails/invite.mjml new file mode 100644 index 0000000000..4eae12d0dc --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/invite.mjml @@ -0,0 +1,49 @@ + + + + + + + + + + + + Join Organization Now + + + This invitation expires on + Tuesday, January 23, 2024 2:59PM UTC. + + + + + + +

+ We’re here for you! +

+ If you have any questions, search the Bitwarden + Help + site or + contact us. +
+
+ + + +
+
+ + +
+
diff --git a/src/Core/MailTemplates/Mjml/emails/two-factor.mjml b/src/Core/MailTemplates/Mjml/emails/two-factor.mjml new file mode 100644 index 0000000000..b959ec1c8a --- /dev/null +++ b/src/Core/MailTemplates/Mjml/emails/two-factor.mjml @@ -0,0 +1,27 @@ + + + + + + + + + + + + +

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

+

Use this code to complete logging in with Bitwarden.

+
+
+
+
+ + +
+
diff --git a/src/Core/MailTemplates/Mjml/package-lock.json b/src/Core/MailTemplates/Mjml/package-lock.json new file mode 100644 index 0000000000..30e1e3568c --- /dev/null +++ b/src/Core/MailTemplates/Mjml/package-lock.json @@ -0,0 +1,2186 @@ +{ + "name": "@bitwarden/mjml-emails", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bitwarden/mjml-emails", + "version": "1.0.0", + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "mjml": "4.15.3", + "mjml-core": "4.15.3" + }, + "devDependencies": { + "nodemon": "3.1.10", + "prettier": "3.5.3" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "license": "MIT", + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/juice": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.1.tgz", + "integrity": "sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==", + "license": "MIT", + "dependencies": { + "cheerio": "1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mjml": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.15.3.tgz", + "integrity": "sha512-bW2WpJxm6HS+S3Yu6tq1DUPFoTxU9sPviUSmnL7Ua+oVO3WA5ILFWqvujUlz+oeuM+HCwEyMiP5xvKNPENVjYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "mjml-cli": "4.15.3", + "mjml-core": "4.15.3", + "mjml-migrate": "4.15.3", + "mjml-preset-core": "4.15.3", + "mjml-validator": "4.15.3" + }, + "bin": { + "mjml": "bin/mjml" + } + }, + "node_modules/mjml-accordion": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.15.3.tgz", + "integrity": "sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-body": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.15.3.tgz", + "integrity": "sha512-7pfUOVPtmb0wC+oUOn4xBsAw4eT5DyD6xqaxj/kssu6RrFXOXgJaVnDPAI9AzIvXJ/5as9QrqRGYAddehwWpHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-button": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.15.3.tgz", + "integrity": "sha512-79qwn9AgdGjJR1vLnrcm2rq2AsAZkKC5JPwffTMG+Nja6zGYpTDZFZ56ekHWr/r1b5WxkukcPj2PdevUug8c+Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-carousel": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.15.3.tgz", + "integrity": "sha512-3ju6I4l7uUhPRrJfN3yK9AMsfHvrYbRkcJ1GRphFHzUj37B2J6qJOQUpzA547Y4aeh69TSb7HFVf1t12ejQxVw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-cli": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.15.3.tgz", + "integrity": "sha512-+V2TDw3tXUVEptFvLSerz125C2ogYl8klIBRY1m5BHd4JvGVf3yhx8N3PngByCzA6PGcv/eydGQN+wy34SHf0Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "chokidar": "^3.0.0", + "glob": "^10.3.10", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "minimatch": "^9.0.3", + "mjml-core": "4.15.3", + "mjml-migrate": "4.15.3", + "mjml-parser-xml": "4.15.3", + "mjml-validator": "4.15.3", + "yargs": "^17.7.2" + }, + "bin": { + "mjml-cli": "bin/mjml" + } + }, + "node_modules/mjml-column": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.15.3.tgz", + "integrity": "sha512-hYdEFdJGHPbZJSEysykrevEbB07yhJGSwfDZEYDSbhQQFjV2tXrEgYcFD5EneMaowjb55e3divSJxU4c5q4Qgw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-core": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.15.3.tgz", + "integrity": "sha512-Dmwk+2cgSD9L9GmTbEUNd8QxkTZtW9P7FN/ROZW/fGZD6Hq6/4TB0zEspg2Ow9eYjZXO2ofOJ3PaQEEShKV0kQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "cheerio": "1.0.0-rc.12", + "detect-node": "^2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^10.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.15.3", + "mjml-parser-xml": "4.15.3", + "mjml-validator": "4.15.3" + } + }, + "node_modules/mjml-divider": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.15.3.tgz", + "integrity": "sha512-vh27LQ9FG/01y0b9ntfqm+GT5AjJnDSDY9hilss2ixIUh0FemvfGRfsGVeV5UBVPBKK7Ffhvfqc7Rciob9Spzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-group": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.15.3.tgz", + "integrity": "sha512-HSu/rKnGZVKFq3ciT46vi1EOy+9mkB0HewO4+P6dP/Y0UerWkN6S3UK11Cxsj0cAp0vFwkPDCdOeEzRdpFEkzA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.15.3.tgz", + "integrity": "sha512-o3mRuuP/MB5fZycjD3KH/uXsnaPl7Oo8GtdbJTKtH1+O/3pz8GzGMkscTKa97l03DAG2EhGrzzLcU2A6eshwFw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-attributes": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.15.3.tgz", + "integrity": "sha512-2ISo0r5ZKwkrvJgDou9xVPxxtXMaETe2AsAA02L89LnbB2KC0N5myNsHV0sEysTw9+CfCmgjAb0GAI5QGpxKkQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-breakpoint": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.15.3.tgz", + "integrity": "sha512-Eo56FA5C2v6ucmWQL/JBJ2z641pLOom4k0wP6CMZI2utfyiJ+e2Uuinj1KTrgDcEvW4EtU9HrfAqLK9UosLZlg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-font": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.15.3.tgz", + "integrity": "sha512-CzV2aDPpiNIIgGPHNcBhgyedKY4SX3BJoTwOobSwZVIlEA6TAWB4Z9WwFUmQqZOgo1AkkiTHPZQvGcEhFFXH6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-html-attributes": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.15.3.tgz", + "integrity": "sha512-MDNDPMBOgXUZYdxhosyrA2kudiGO8aogT0/cODyi2Ed9o/1S7W+je11JUYskQbncqhWKGxNyaP4VWa+6+vUC/g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-preview": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.15.3.tgz", + "integrity": "sha512-J2PxCefUVeFwsAExhrKo4lwxDevc5aKj888HBl/wN4EuWOoOg06iOGCxz4Omd8dqyFsrqvbBuPqRzQ+VycGmaA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-style": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.15.3.tgz", + "integrity": "sha512-9J+JuH+mKrQU65CaJ4KZegACUgNIlYmWQYx3VOBR/tyz+8kDYX7xBhKJCjQ1I4wj2Tvga3bykd89Oc2kFZ5WOw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-head-title": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.15.3.tgz", + "integrity": "sha512-IM59xRtsxID4DubQ0iLmoCGXguEe+9BFG4z6y2xQDrscIa4QY3KlfqgKGT69ojW+AVbXXJPEVqrAi4/eCsLItQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-hero": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.15.3.tgz", + "integrity": "sha512-9cLAPuc69yiuzNrMZIN58j+HMK1UWPaq2i3/Fg2ZpimfcGFKRcPGCbEVh0v+Pb6/J0+kf8yIO0leH20opu3AyQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-image": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.15.3.tgz", + "integrity": "sha512-g1OhSdofIytE9qaOGdTPmRIp7JsCtgO0zbsn1Fk6wQh2gEL55Z40j/VoghslWAWTgT2OHFdBKnMvWtN6U5+d2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-migrate": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.15.3.tgz", + "integrity": "sha512-sr/+35RdxZroNQVegjpfRHJ5hda9XCgaS4mK2FGO+Mb1IUevKfeEPII3F/cHDpNwFeYH3kAgyqQ22ClhGLWNBA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.15.3", + "mjml-parser-xml": "4.15.3", + "yargs": "^17.7.2" + }, + "bin": { + "migrate": "lib/cli.js" + } + }, + "node_modules/mjml-navbar": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.15.3.tgz", + "integrity": "sha512-VsKH/Jdlf8Yu3y7GpzQV5n7JMdpqvZvTSpF6UQXL0PWOm7k6+LX+sCZimOfpHJ+wCaaybpxokjWZ71mxOoCWoA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-parser-xml": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.15.3.tgz", + "integrity": "sha512-Tz0UX8/JVYICLjT+U8J1f/TFxIYVYjzZHeh4/Oyta0pLpRLeZlxEd71f3u3kdnulCKMP4i37pFRDmyLXAlEuLw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "detect-node": "2.1.0", + "htmlparser2": "^9.1.0", + "lodash": "^4.17.15" + } + }, + "node_modules/mjml-parser-xml/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/mjml-preset-core": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.15.3.tgz", + "integrity": "sha512-1zZS8P4O0KweWUqNS655+oNnVMPQ1Rq1GaZq5S9JfwT1Vh/m516lSmiTW9oko6gGHytt5s6Yj6oOeu5Zm8FoLw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "mjml-accordion": "4.15.3", + "mjml-body": "4.15.3", + "mjml-button": "4.15.3", + "mjml-carousel": "4.15.3", + "mjml-column": "4.15.3", + "mjml-divider": "4.15.3", + "mjml-group": "4.15.3", + "mjml-head": "4.15.3", + "mjml-head-attributes": "4.15.3", + "mjml-head-breakpoint": "4.15.3", + "mjml-head-font": "4.15.3", + "mjml-head-html-attributes": "4.15.3", + "mjml-head-preview": "4.15.3", + "mjml-head-style": "4.15.3", + "mjml-head-title": "4.15.3", + "mjml-hero": "4.15.3", + "mjml-image": "4.15.3", + "mjml-navbar": "4.15.3", + "mjml-raw": "4.15.3", + "mjml-section": "4.15.3", + "mjml-social": "4.15.3", + "mjml-spacer": "4.15.3", + "mjml-table": "4.15.3", + "mjml-text": "4.15.3", + "mjml-wrapper": "4.15.3" + } + }, + "node_modules/mjml-raw": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.15.3.tgz", + "integrity": "sha512-IGyHheOYyRchBLiAEgw3UM11kFNmBSMupu2BDdejC6ZiDhEAdG+tyERlsCwDPYtXanvFpGWULIu3XlsUPc+RZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-section": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.15.3.tgz", + "integrity": "sha512-JfVPRXH++Hd933gmQfG8JXXCBCR6fIzC3DwiYycvanL/aW1cEQ2EnebUfQkt5QzlYjOkJEH+JpccAsq3ln6FZQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-social": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.15.3.tgz", + "integrity": "sha512-7sD5FXrESOxpT9Z4Oh36bS6u/geuUrMP1aCg2sjyAwbPcF1aWa2k9OcatQfpRf6pJEhUZ18y6/WBBXmMVmSzXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-spacer": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.15.3.tgz", + "integrity": "sha512-3B7Qj+17EgDdAtZ3NAdMyOwLTX1jfmJuY7gjyhS2HtcZAmppW+cxqHUBwCKfvSRgTQiccmEvtNxaQK+tfyrZqA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-table": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.15.3.tgz", + "integrity": "sha512-FLx7DcRKTdKdcOCbMyBaeudeHaHpwPveRrBm6WyQe3LXx6FfdmOh59i71/16LFQMgBOD3N4/UJkzxLzlTJzMqQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-text": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.15.3.tgz", + "integrity": "sha512-+C0hxCmw9kg0XzT6vhE5mFkK6y225nC8UEQcN94K0fBCjPKkM+HqZMwGX205fzdGRi+Bxa55b/VhrIVwdv+8vw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3" + } + }, + "node_modules/mjml-validator": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.15.3.tgz", + "integrity": "sha512-Xb72KdqRwjv/qM2rJpV22syyP2N3cRQ9VVDrN6u2FSzLq02buFNxmSPJ7CKhat3PrUNdVHU75KZwOf/tz4UEhA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + } + }, + "node_modules/mjml-wrapper": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.15.3.tgz", + "integrity": "sha512-ditsCijeHJrmBmObtJmQ18ddLxv5oPyMTdPU8Di8APOnD2zPk7Z4UAuJSl7HXB45oFiivr3MJf4koFzMUSZ6Gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "lodash": "^4.17.21", + "mjml-core": "4.15.3", + "mjml-section": "4.15.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "license": "MIT (http://mootools.net/license.txt)", + "engines": { + "node": "*" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "license": "MIT" + }, + "node_modules/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/src/Core/MailTemplates/Mjml/package.json b/src/Core/MailTemplates/Mjml/package.json new file mode 100644 index 0000000000..c3690a2d73 --- /dev/null +++ b/src/Core/MailTemplates/Mjml/package.json @@ -0,0 +1,30 @@ +{ + "name": "@bitwarden/mjml-emails", + "version": "1.0.0", + "description": "Email templates for Bitwarden", + "private": true, + "type": "commonjs", + "repository": { + "type": "git", + "url": "git+https://github.com/bitwarden/server.git" + }, + "author": "Bitwarden Inc. (https://bitwarden.com)", + "license": "SEE LICENSE IN LICENSE.txt", + "bugs": { + "url": "https://github.com/bitwarden/server/issues" + }, + "homepage": "https://bitwarden.com", + "scripts": { + "build": "./build.sh", + "watch": "nodemon --exec ./build.sh --watch ./components --watch ./emails --ext js,mjml", + "prettier": "prettier --cache --write ." + }, + "dependencies": { + "mjml": "4.15.3", + "mjml-core": "4.15.3" + }, + "devDependencies": { + "nodemon": "3.1.10", + "prettier": "3.5.3" + } +} From 5270fba44dfe130af1789b55f04ff33040ad6377 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:02:51 -0400 Subject: [PATCH 058/326] [deps] Auth: Update sass to v1.89.2 (#5863) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 636d6317a1..a6db196d48 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.88.0", + "sass": "1.89.2", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" @@ -1860,9 +1860,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", - "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 137f86680c..064cf6d656 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -16,7 +16,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.88.0", + "sass": "1.89.2", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index e73ccfcef5..b3f19c4792 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -18,7 +18,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.88.0", + "sass": "1.89.2", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" @@ -1861,9 +1861,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", - "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index e88cd42eca..9076a46239 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.88.0", + "sass": "1.89.2", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" From 45370623e9fa4bca2457587ba6a749b9b14c8411 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:12:09 +0200 Subject: [PATCH 059/326] Feature flag for ForceUpdateKDFSettings (#6087) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ad726a40fe..d31d141431 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -171,6 +171,7 @@ public static class FeatureFlagKeys public const string SSHKeyItemVaultItem = "ssh-key-vault-item"; public const string UserSdkForDecryption = "use-sdk-for-decryption"; public const string PM17987_BlockType0 = "pm-17987-block-type-0"; + public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings"; /* Mobile Team */ public const string NativeCarouselFlow = "native-carousel-flow"; From e9d44037739dbd717a170361f706e247d51c410d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:36:22 +0100 Subject: [PATCH 060/326] [PM-20167] Refactor: Remove flagged logic for FeatureFlagKeys.SeparateCustomRolePermissions --- .../OrganizationUsersController.cs | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5409adc825..55f1c9de14 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -11,12 +11,10 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -168,43 +166,6 @@ public class OrganizationUsersController : Controller [HttpGet("")] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) - { - - if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions)) - { - return await GetvNextAsync(orgId, includeGroups, includeCollections); - } - - var authorized = (await _authorizationService.AuthorizeAsync( - User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; - if (!authorized) - { - throw new NotFoundException(); - } - - var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails( - new OrganizationUserUserDetailsQueryRequest - { - OrganizationId = orgId, - IncludeGroups = includeGroups, - IncludeCollections = includeCollections - } - ); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); - var organizationUsersClaimedStatus = await GetClaimedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id)); - var responses = organizationUsers - .Select(o => - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled; - var claimedByOrganization = organizationUsersClaimedStatus[o.Id]; - var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, claimedByOrganization); - - return orgUser; - }); - return new ListResponseModel(responses); - } - - private async Task> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false) { var request = new OrganizationUserUserDetailsQueryRequest { From 5fc7f4700c642dcaaa56a716c803787e05608dde Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:41:08 -0400 Subject: [PATCH 061/326] [PM-17562] Add in-memory cache for event integrations (#6085) * [PM-17562] Add in-memory cache for event integrations * Fix Sql error * Fix failing test * Add additional tests for new cache service * PR suggestions addressed --- ...nizationIntegrationConfigurationDetails.cs | 1 + ...ationIntegrationConfigurationRepository.cs | 2 + .../IIntegrationConfigurationDetailsCache.cs | 14 ++ .../EventIntegrationHandler.cs | 4 +- ...grationConfigurationDetailsCacheService.cs | 73 ++++++++++ .../EventIntegrations/README.md | 29 ++++ src/Core/Settings/GlobalSettings.cs | 1 + ...ationIntegrationConfigurationRepository.cs | 12 ++ ...ationIntegrationConfigurationRepository.cs | 10 ++ ...tTypeOrganizationIdIntegrationTypeQuery.cs | 5 +- ...rationConfigurationDetailsReadManyQuery.cs | 28 ++++ .../Utilities/ServiceCollectionExtensions.cs | 13 +- ...tegrationConfigurationDetails_ReadMany.sql | 11 ++ .../Services/EventIntegrationHandlerTests.cs | 6 +- ...onConfigurationDetailsCacheServiceTests.cs | 133 ++++++++++++++++++ ...izationIntegrationConfigurationDetails.sql | 11 ++ 16 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadMany.sql create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs create mode 100644 util/Migrator/DbScripts/2025-07-11_00_AddReadManyOrganizationIntegrationConfigurationDetails.sql diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs index b5d224c012..a184e9ac8e 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Models.Data.Organizations; public class OrganizationIntegrationConfigurationDetails { public Guid Id { get; set; } + public Guid OrganizationId { get; set; } public Guid OrganizationIntegrationId { get; set; } public IntegrationType IntegrationType { get; set; } public EventType EventType { get; set; } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs index 516918fff9..53159c98e7 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -10,4 +10,6 @@ public interface IOrganizationIntegrationConfigurationRepository : IRepository> GetAllConfigurationDetailsAsync(); } diff --git a/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs b/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs new file mode 100644 index 0000000000..ad27429112 --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationConfigurationDetailsCache.cs @@ -0,0 +1,14 @@ +#nullable enable + +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Core.Services; + +public interface IIntegrationConfigurationDetailsCache +{ + List GetConfigurationDetails( + Guid organizationId, + IntegrationType integrationType, + EventType eventType); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 3ffd08edd2..9cd789be76 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -14,7 +14,7 @@ public class EventIntegrationHandler( IntegrationType integrationType, IEventIntegrationPublisher eventIntegrationPublisher, IIntegrationFilterService integrationFilterService, - IOrganizationIntegrationConfigurationRepository configurationRepository, + IIntegrationConfigurationDetailsCache configurationCache, IUserRepository userRepository, IOrganizationRepository organizationRepository, ILogger> logger) @@ -27,7 +27,7 @@ public class EventIntegrationHandler( return; } - var configurations = await configurationRepository.GetConfigurationDetailsAsync( + var configurations = configurationCache.GetConfigurationDetails( organizationId, integrationType, eventMessage.Type); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs new file mode 100644 index 0000000000..4e4657f824 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class IntegrationConfigurationDetailsCacheService : BackgroundService, IIntegrationConfigurationDetailsCache +{ + private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType EventType); + private readonly IOrganizationIntegrationConfigurationRepository _repository; + private readonly ILogger _logger; + private readonly TimeSpan _refreshInterval; + private Dictionary> _cache = new(); + + public IntegrationConfigurationDetailsCacheService( + IOrganizationIntegrationConfigurationRepository repository, + GlobalSettings globalSettings, + ILogger logger + ) + { + _repository = repository; + _logger = logger; + _refreshInterval = TimeSpan.FromMinutes(globalSettings.EventLogging.IntegrationCacheRefreshIntervalMinutes); + } + + public List GetConfigurationDetails( + Guid organizationId, + IntegrationType integrationType, + EventType eventType) + { + var key = new IntegrationCacheKey(organizationId, integrationType, eventType); + return _cache.TryGetValue(key, out var value) + ? value + : new List(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await RefreshAsync(); + + var timer = new PeriodicTimer(_refreshInterval); + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await RefreshAsync(); + } + } + + internal async Task RefreshAsync() + { + var stopwatch = Stopwatch.StartNew(); + try + { + var newCache = (await _repository.GetAllConfigurationDetailsAsync()) + .GroupBy(x => new IntegrationCacheKey(x.OrganizationId, x.IntegrationType, x.EventType)) + .ToDictionary(g => g.Key, g => g.ToList()); + _cache = newCache; + + stopwatch.Stop(); + _logger.LogInformation( + "[IntegrationConfigurationDetailsCacheService] Refreshed successfully: {Count} entries in {Duration}ms", + newCache.Count, + stopwatch.Elapsed.TotalMilliseconds); + } + catch (Exception ex) + { + _logger.LogError("[IntegrationConfigurationDetailsCacheService] Refresh failed: {ex}", ex); + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index b2327b0f75..f48dee8aad 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -290,6 +290,35 @@ graph TD C1 -->|Has many| B1_2[IntegrationFilterRule] C1 -->|Can contain| C2[IntegrationFilterGroup...] ``` +## Caching + +To reduce database load and improve performance, integration configurations are cached in-memory as a Dictionary +with a periodic load of all configurations. Without caching, each incoming `EventMessage` would trigger a database +query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`. + +By loading all configurations into memory on a fixed interval, we ensure: + +- Consistent performance for reads. +- Reduced database pressure. +- Predictable refresh timing, independent of event activity. + +### Architecture / Design + +- The cache is read-only for consumers. It is only updated in bulk by a background refresh process. +- The cache is fully replaced on each refresh to avoid locking or partial state. +- Reads return a `List` for a given key or an empty list if no + match exists. +- Failures or delays in the loading process do not affect the existing cache state. The cache will continue serving + the last known good state until the update replaces the whole cache. + +### Background Refresh + +A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the background and: + +- Loads all configuration records at application startup. +- Refreshes the cache on a configurable interval. +- Logs timing and entry count on success. +- Logs exceptions on failure without disrupting application flow. # Building a new integration diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index e4f308c358..e49ef4a7f2 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -287,6 +287,7 @@ public class GlobalSettings : IGlobalSettings { public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); + public int IntegrationCacheRefreshIntervalMinutes { get; set; } = 10; public class AzureServiceBusSettings { diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs index f3227dfd22..5a7e1ce152 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -40,4 +40,16 @@ public class OrganizationIntegrationConfigurationRepository : Repository> GetAllConfigurationDetailsAsync() + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationIntegrationConfigurationDetails_ReadMany]", + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs index f051830035..1e1dcd3ba4 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -30,4 +30,14 @@ public class OrganizationIntegrationConfigurationRepository : Repository> GetAllConfigurationDetailsAsync() + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new OrganizationIntegrationConfigurationDetailsReadManyQuery(); + return await query.Run(dbContext).ToListAsync(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs index c816b01a01..b4441c5084 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs @@ -1,4 +1,6 @@ -using Bit.Core.Enums; +#nullable enable + +using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; @@ -27,6 +29,7 @@ public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrgan select new OrganizationIntegrationConfigurationDetails() { Id = oic.Id, + OrganizationId = oi.OrganizationId, OrganizationIntegrationId = oic.OrganizationIntegrationId, IntegrationType = oi.Type, EventType = oic.EventType, diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs new file mode 100644 index 0000000000..8141292c81 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyQuery.cs @@ -0,0 +1,28 @@ +#nullable enable + +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class OrganizationIntegrationConfigurationDetailsReadManyQuery : IQuery +{ + public IQueryable Run(DatabaseContext dbContext) + { + var query = from oic in dbContext.OrganizationIntegrationConfigurations + join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id into oioic + from oi in dbContext.OrganizationIntegrations + select new OrganizationIntegrationConfigurationDetails() + { + Id = oic.Id, + OrganizationId = oi.OrganizationId, + OrganizationIntegrationId = oic.OrganizationIntegrationId, + IntegrationType = oi.Type, + EventType = oic.EventType, + Configuration = oic.Configuration, + Filters = oic.Filters, + IntegrationConfiguration = oi.Configuration, + Template = oic.Template + }; + return query; + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 0bf09706a9..dad0bc230e 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -618,7 +618,7 @@ public static class ServiceCollectionExtensions integrationType, provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService>>())); @@ -652,6 +652,10 @@ public static class ServiceCollectionExtensions !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) return services; + services.AddSingleton(); + services.AddSingleton(provider => + provider.GetRequiredService()); + services.AddHostedService(provider => provider.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -664,6 +668,7 @@ public static class ServiceCollectionExtensions integrationType: IntegrationType.Slack, globalSettings: globalSettings); + services.TryAddSingleton(TimeProvider.System); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); services.AddAzureServiceBusIntegration( eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName, @@ -711,7 +716,7 @@ public static class ServiceCollectionExtensions integrationType, provider.GetRequiredService(), provider.GetRequiredService(), - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService>>())); @@ -745,6 +750,10 @@ public static class ServiceCollectionExtensions return services; } + services.AddSingleton(); + services.AddSingleton(provider => + provider.GetRequiredService()); + services.AddHostedService(provider => provider.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadMany.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadMany.sql new file mode 100644 index 0000000000..9c65ef58af --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfigurationDetails_ReadMany.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadMany] +AS +BEGIN + SET NOCOUNT ON + + SELECT + oic.* + FROM + [dbo].[OrganizationIntegrationConfigurationDetailsView] oic +END +GO diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 2d1099db65..f038fe28ef 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -32,12 +32,12 @@ public class EventIntegrationHandlerTests private SutProvider> GetSutProvider( List configurations) { - var configurationRepository = Substitute.For(); - configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), + var configurationCache = Substitute.For(); + configurationCache.GetConfigurationDetails(Arg.Any(), IntegrationType.Webhook, Arg.Any()).Returns(configurations); return new SutProvider>() - .SetDependency(configurationRepository) + .SetDependency(configurationCache) .SetDependency(_eventIntegrationPublisher) .SetDependency(IntegrationType.Webhook) .SetDependency(_logger) diff --git a/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs new file mode 100644 index 0000000000..d24a5afa27 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs @@ -0,0 +1,133 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class IntegrationConfigurationDetailsCacheServiceTests +{ + private SutProvider GetSutProvider( + List configurations) + { + var configurationRepository = Substitute.For(); + configurationRepository.GetAllConfigurationDetailsAsync().Returns(configurations); + + return new SutProvider() + .SetDependency(configurationRepository) + .Create(); + } + + [Theory, BitAutoData] + public async Task GetConfigurationDetails_KeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config) + { + var sutProvider = GetSutProvider([config]); + await sutProvider.Sut.RefreshAsync(); + var result = sutProvider.Sut.GetConfigurationDetails( + config.OrganizationId, + config.IntegrationType, + config.EventType); + Assert.Single(result); + Assert.Same(config, result[0]); + } + + [Theory, BitAutoData] + public async Task GetConfigurationDetails_KeyMissing_ReturnsEmptyList(OrganizationIntegrationConfigurationDetails config) + { + var sutProvider = GetSutProvider([config]); + await sutProvider.Sut.RefreshAsync(); + var result = sutProvider.Sut.GetConfigurationDetails( + Guid.NewGuid(), + config.IntegrationType, + config.EventType); + Assert.Empty(result); + } + + [Theory, BitAutoData] + public async Task GetConfigurationDetails_ReturnsCachedValue_EvenIfRepositoryChanges(OrganizationIntegrationConfigurationDetails config) + { + var sutProvider = GetSutProvider([config]); + await sutProvider.Sut.RefreshAsync(); + + var newConfig = JsonSerializer.Deserialize(JsonSerializer.Serialize(config)); + Assert.NotNull(newConfig); + newConfig.Template = "Changed"; + sutProvider.GetDependency().GetAllConfigurationDetailsAsync() + .Returns([newConfig]); + + var result = sutProvider.Sut.GetConfigurationDetails( + config.OrganizationId, + config.IntegrationType, + config.EventType); + Assert.Single(result); + Assert.NotEqual("Changed", result[0].Template); // should not yet pick up change from repository + + await sutProvider.Sut.RefreshAsync(); // Pick up changes + + result = sutProvider.Sut.GetConfigurationDetails( + config.OrganizationId, + config.IntegrationType, + config.EventType); + Assert.Single(result); + Assert.Equal("Changed", result[0].Template); // Should have the new value + } + + [Theory, BitAutoData] + public async Task RefreshAsync_GroupsByCompositeKey(OrganizationIntegrationConfigurationDetails config1) + { + var config2 = JsonSerializer.Deserialize( + JsonSerializer.Serialize(config1))!; + config2.Template = "Another"; + + var sutProvider = GetSutProvider([config1, config2]); + await sutProvider.Sut.RefreshAsync(); + + var results = sutProvider.Sut.GetConfigurationDetails( + config1.OrganizationId, + config1.IntegrationType, + config1.EventType); + + Assert.Equal(2, results.Count); + Assert.Contains(results, r => r.Template == config1.Template); + Assert.Contains(results, r => r.Template == config2.Template); + } + + [Theory, BitAutoData] + public async Task RefreshAsync_LogsInformationOnSuccess(OrganizationIntegrationConfigurationDetails config) + { + var sutProvider = GetSutProvider([config]); + await sutProvider.Sut.RefreshAsync(); + + sutProvider.GetDependency>().Received().Log( + LogLevel.Information, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Refreshed successfully")), + null, + Arg.Any>()); + } + + [Fact] + public async Task RefreshAsync_OnException_LogsError() + { + var sutProvider = GetSutProvider([]); + sutProvider.GetDependency().GetAllConfigurationDetailsAsync() + .Throws(new Exception("Database failure")); + await sutProvider.Sut.RefreshAsync(); + + sutProvider.GetDependency>().Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains("Refresh failed")), + Arg.Any(), + Arg.Any>()); + } +} diff --git a/util/Migrator/DbScripts/2025-07-11_00_AddReadManyOrganizationIntegrationConfigurationDetails.sql b/util/Migrator/DbScripts/2025-07-11_00_AddReadManyOrganizationIntegrationConfigurationDetails.sql new file mode 100644 index 0000000000..475755b8eb --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-11_00_AddReadManyOrganizationIntegrationConfigurationDetails.sql @@ -0,0 +1,11 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadMany] +AS +BEGIN + SET NOCOUNT ON + + SELECT + oic.* + FROM + [dbo].[OrganizationIntegrationConfigurationDetailsView] oic +END +GO From 9a501f95c883d28e08b146004d96fb15330c2440 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 17 Jul 2025 09:55:21 -0400 Subject: [PATCH 062/326] Move more SQL files that were placed in the wrong location (#6094) --- .../MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql | 0 .../dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Sql/{Dirt/Stored Procedure => dbo/Dirt/Stored Procedures}/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql (100%) rename src/Sql/{NotificationCenter => }/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql (100%) diff --git a/src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql similarity index 100% rename from src/Sql/Dirt/Stored Procedure/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql rename to src/Sql/dbo/Dirt/Stored Procedures/MemberAccessDetail_GetMemberAccessDetailByOrganizationId.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql b/src/Sql/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql rename to src/Sql/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql From ec70a18bda689994a5b061837d430b2d8a1011e3 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:02:25 -0500 Subject: [PATCH 063/326] [NO LOGIC] [PM-21100] Organize billing organization code (#6099) * [NO LOGIC] Organize Billing organization code * Run dotnet format --- src/Admin/Controllers/ToolsController.cs | 10 +++++----- .../Organizations/OrganizationResponseModel.cs | 2 +- .../Billing/Controllers/LicensesController.cs | 11 ++++++----- .../Controllers/OrganizationBillingController.cs | 3 ++- .../Controllers/OrganizationsController.cs | 12 ++++++------ .../Responses/OrganizationMetadataResponse.cs | 2 +- .../SelfHostedOrganizationLicensesController.cs | 13 +++++++------ src/Core/AdminConsole/Entities/Organization.cs | 2 +- .../SelfHostedOrganizationDetails.cs | 2 +- .../CloudOrganizationSignUpCommand.cs | 4 ++-- .../Services/IOrganizationService.cs | 2 +- .../Implementations/OrganizationService.cs | 2 +- .../AdminConsole/Services/OrganizationFactory.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 10 +++++----- .../Interfaces/IGetOrganizationLicenseQuery.cs | 16 ---------------- .../IUpdateOrganizationLicenseCommand.cs | 13 ------------- .../UpdateOrganizationLicenseCommand.cs | 15 +++++++++------ .../Entities/OrganizationInstallation.cs | 4 +--- .../Models}/OrganizationLicense.cs | 3 ++- .../Models/OrganizationMetadata.cs | 2 +- .../Models}/OrganizationSale.cs | 6 +++--- .../SponsorOrganizationSubscriptionUpdate.cs | 2 +- .../Queries/GetCloudOrganizationLicenseQuery.cs} | 15 ++++++++++----- .../GetSelfHostedOrganizationLicenseQuery.cs} | 14 +++++++++----- .../IOrganizationInstallationRepository.cs | 4 ++-- .../Services/IOrganizationBillingService.cs | 6 ++---- .../Services}/OrganizationBillingService.cs | 7 +++---- src/Core/Billing/Services/ILicensingService.cs | 1 + .../Services/Implementations/LicensingService.cs | 1 + .../NoopImplementations/NoopLicensingService.cs | 1 + .../UpgradeOrganizationPlanCommand.cs | 4 ++-- .../Implementations/StripePaymentService.cs | 2 +- .../OrganizationInstallationRepository.cs | 4 ++-- .../DapperServiceCollectionExtensions.cs | 2 +- .../Billing/Models/OrganizationInstallation.cs | 4 ++-- .../OrganizationInstallationRepository.cs | 4 ++-- ...EntityFrameworkServiceCollectionExtensions.cs | 2 +- .../OrganizationConnectionsControllerTests.cs | 2 +- .../OrganizationBillingControllerTests.cs | 3 ++- .../Controllers/OrganizationsControllerTests.cs | 10 +++++----- test/Api.Test/Utilities/ApiHelpersTests.cs | 2 +- .../Data/SelfHostedOrganizationDetailsTests.cs | 2 +- .../CloudOrganizationSignUpCommandTests.cs | 4 ++-- .../OrganizationLicenseCustomization.cs | 2 +- .../Business/OrganizationLicenseFileFixtures.cs | 2 +- .../Models/Business/OrganizationLicenseTests.cs | 2 +- .../CloudGetOrganizationLicenseQueryTests.cs | 14 +++++++------- ...SelfHostedGetOrganizationLicenseQueryTests.cs | 10 +++++----- .../UpdateOrganizationLicenseCommandTests.cs | 4 ++-- .../Billing/Services/LicensingServiceTests.cs | 2 +- .../Services/OrganizationBillingServiceTests.cs | 2 +- .../Factories/WebApplicationFactoryBase.cs | 1 + 52 files changed, 129 insertions(+), 142 deletions(-) delete mode 100644 src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs delete mode 100644 src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs rename src/Core/Billing/{OrganizationFeatures/OrganizationLicenses => Organizations/Commands}/UpdateOrganizationLicenseCommand.cs (88%) rename src/Core/Billing/{ => Organizations}/Entities/OrganizationInstallation.cs (90%) rename src/Core/Billing/{Models/Business => Organizations/Models}/OrganizationLicense.cs (99%) rename src/Core/Billing/{ => Organizations}/Models/OrganizationMetadata.cs (92%) rename src/Core/Billing/{Models/Sales => Organizations/Models}/OrganizationSale.cs (96%) rename src/Core/Billing/{Models/Business => Organizations/Models}/SponsorOrganizationSubscriptionUpdate.cs (98%) rename src/Core/Billing/{OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs => Organizations/Queries/GetCloudOrganizationLicenseQuery.cs} (85%) rename src/Core/Billing/{OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs => Organizations/Queries/GetSelfHostedOrganizationLicenseQuery.cs} (79%) rename src/Core/Billing/{ => Organizations}/Repositories/IOrganizationInstallationRepository.cs (74%) rename src/Core/Billing/{ => Organizations}/Services/IOrganizationBillingService.cs (96%) rename src/Core/Billing/{Services/Implementations => Organizations/Services}/OrganizationBillingService.cs (99%) diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index fedb96be46..b754b1f968 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -8,7 +8,7 @@ using Bit.Admin.Models; using Bit.Admin.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Platform.Installations; @@ -27,7 +27,7 @@ public class ToolsController : Controller { private readonly GlobalSettings _globalSettings; private readonly IOrganizationRepository _organizationRepository; - private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; + private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery; private readonly IUserService _userService; private readonly ITransactionRepository _transactionRepository; private readonly IInstallationRepository _installationRepository; @@ -40,7 +40,7 @@ public class ToolsController : Controller public ToolsController( GlobalSettings globalSettings, IOrganizationRepository organizationRepository, - ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, + IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery, IUserService userService, ITransactionRepository transactionRepository, IInstallationRepository installationRepository, @@ -52,7 +52,7 @@ public class ToolsController : Controller { _globalSettings = globalSettings; _organizationRepository = organizationRepository; - _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; + _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery; _userService = userService; _transactionRepository = transactionRepository; _installationRepository = installationRepository; @@ -320,7 +320,7 @@ public class ToolsController : Controller if (organization != null) { - var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, + var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, model.InstallationId.Value, model.Version); var ms = new MemoryStream(); await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented); diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index d532975388..b34765fb19 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -5,7 +5,7 @@ using System.Text.Json.Serialization; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; diff --git a/src/Api/Billing/Controllers/LicensesController.cs b/src/Api/Billing/Controllers/LicensesController.cs index 1a19fd27e0..29313bd4d8 100644 --- a/src/Api/Billing/Controllers/LicensesController.cs +++ b/src/Api/Billing/Controllers/LicensesController.cs @@ -3,7 +3,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api.OrganizationLicenses; @@ -23,7 +24,7 @@ public class LicensesController : Controller private readonly IUserRepository _userRepository; private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; - private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; + private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery; private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; private readonly ICurrentContext _currentContext; @@ -31,14 +32,14 @@ public class LicensesController : Controller IUserRepository userRepository, IUserService userService, IOrganizationRepository organizationRepository, - ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, + IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery, IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, ICurrentContext currentContext) { _userRepository = userRepository; _userService = userService; _organizationRepository = organizationRepository; - _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; + _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery; _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; _currentContext = currentContext; } @@ -84,7 +85,7 @@ public class LicensesController : Controller throw new BadRequestException("Invalid Billing Sync Key"); } - var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value); + var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value); return license; } } diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index f1ab1be6bd..50a302f6d2 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -6,7 +6,8 @@ using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Queries.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index ca13690b5c..977b20bdfb 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -9,12 +9,12 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Entities; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; @@ -39,7 +39,7 @@ public class OrganizationsController( IUserService userService, IPaymentService paymentService, ICurrentContext currentContext, - ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, + IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery, GlobalSettings globalSettings, ILicensingService licensingService, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, @@ -98,7 +98,7 @@ public class OrganizationsController( } var org = await organizationRepository.GetByIdAsync(id); - var license = await cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId); + var license = await getCloudOrganizationLicenseQuery.GetLicenseAsync(org, installationId); if (license == null) { throw new NotFoundException(); diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 341dbceadf..a13f267c3b 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Organizations.Models; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index bdd8aaba84..d0411f9bc5 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -5,8 +5,9 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Organizations; using Bit.Api.Utilities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Commands; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -25,7 +26,7 @@ namespace Bit.Api.Controllers.SelfHosted; public class SelfHostedOrganizationLicensesController : Controller { private readonly ICurrentContext _currentContext; - private readonly ISelfHostedGetOrganizationLicenseQuery _selfHostedGetOrganizationLicenseQuery; + private readonly IGetSelfHostedOrganizationLicenseQuery _getSelfHostedOrganizationLicenseQuery; private readonly IOrganizationConnectionRepository _organizationConnectionRepository; private readonly IOrganizationService _organizationService; private readonly IOrganizationRepository _organizationRepository; @@ -34,7 +35,7 @@ public class SelfHostedOrganizationLicensesController : Controller public SelfHostedOrganizationLicensesController( ICurrentContext currentContext, - ISelfHostedGetOrganizationLicenseQuery selfHostedGetOrganizationLicenseQuery, + IGetSelfHostedOrganizationLicenseQuery getSelfHostedOrganizationLicenseQuery, IOrganizationConnectionRepository organizationConnectionRepository, IOrganizationService organizationService, IOrganizationRepository organizationRepository, @@ -42,7 +43,7 @@ public class SelfHostedOrganizationLicensesController : Controller IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand) { _currentContext = currentContext; - _selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery; + _getSelfHostedOrganizationLicenseQuery = getSelfHostedOrganizationLicenseQuery; _organizationConnectionRepository = organizationConnectionRepository; _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -120,7 +121,7 @@ public class SelfHostedOrganizationLicensesController : Controller } var license = - await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(selfHostedOrganizationDetails, billingSyncConnection); + await _getSelfHostedOrganizationLicenseQuery.GetLicenseAsync(selfHostedOrganizationDetails, billingSyncConnection); var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey); await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization); diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 9d506b4251..8ab3af3c1e 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -4,7 +4,7 @@ using System.Text.Json; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index de28c4ba80..84ff164943 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -6,7 +6,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 96fcc087e6..8d8ab8cdfc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -5,9 +5,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; 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.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 1379c06bc3..2fa6772c62 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 5daefffea0..3e81494fc3 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -20,7 +20,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs index 2d26e3d156..dbc8f0fa21 100644 --- a/src/Core/AdminConsole/Services/OrganizationFactory.cs +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Licenses.Extensions; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 944c29e90f..3fb5526254 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,9 @@ using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches.Implementations; using Bit.Core.Billing.Licenses.Extensions; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; +using Bit.Core.Billing.Organizations.Commands; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Payment; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; @@ -37,8 +37,8 @@ public static class ServiceCollectionExtensions private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); } } diff --git a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs deleted file mode 100644 index 147bee398f..0000000000 --- a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Entities; - -namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; - -public interface ICloudGetOrganizationLicenseQuery -{ - Task GetLicenseAsync(Organization organization, Guid installationId, - int? version = null); -} - -public interface ISelfHostedGetOrganizationLicenseQuery -{ - Task GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection); -} diff --git a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs b/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs deleted file mode 100644 index c9c71f37a4..0000000000 --- a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Interfaces/IUpdateOrganizationLicenseCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Models.Data.Organizations; - -namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; - -public interface IUpdateOrganizationLicenseCommand -{ - Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization, - OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey); -} diff --git a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs b/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs similarity index 88% rename from src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs rename to src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs index 364571437a..fde95f2e70 100644 --- a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs @@ -1,9 +1,6 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations; @@ -11,7 +8,13 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Billing.Organizations.Commands; + +public interface IUpdateOrganizationLicenseCommand +{ + Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization, + OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey); +} public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseCommand { diff --git a/src/Core/Billing/Entities/OrganizationInstallation.cs b/src/Core/Billing/Organizations/Entities/OrganizationInstallation.cs similarity index 90% rename from src/Core/Billing/Entities/OrganizationInstallation.cs rename to src/Core/Billing/Organizations/Entities/OrganizationInstallation.cs index 4332afd44a..98930ae805 100644 --- a/src/Core/Billing/Entities/OrganizationInstallation.cs +++ b/src/Core/Billing/Organizations/Entities/OrganizationInstallation.cs @@ -1,9 +1,7 @@ using Bit.Core.Entities; using Bit.Core.Utilities; -namespace Bit.Core.Billing.Entities; - -#nullable enable +namespace Bit.Core.Billing.Organizations.Entities; public class OrganizationInstallation : ITableObject { diff --git a/src/Core/Billing/Models/Business/OrganizationLicense.cs b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs similarity index 99% rename from src/Core/Billing/Models/Business/OrganizationLicense.cs rename to src/Core/Billing/Organizations/Models/OrganizationLicense.cs index 2567c274e9..cd90cb517e 100644 --- a/src/Core/Billing/Models/Business/OrganizationLicense.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs @@ -10,12 +10,13 @@ using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Settings; -namespace Bit.Core.Billing.Models.Business; +namespace Bit.Core.Billing.Organizations.Models; public class OrganizationLicense : ILicense { diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs similarity index 92% rename from src/Core/Billing/Models/OrganizationMetadata.cs rename to src/Core/Billing/Organizations/Models/OrganizationMetadata.cs index 0f2bf9a454..2bcd213dbf 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationMetadata.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Organizations.Models; public record OrganizationMetadata( bool IsEligibleForSelfHost, diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Organizations/Models/OrganizationSale.cs similarity index 96% rename from src/Core/Billing/Models/Sales/OrganizationSale.cs rename to src/Core/Billing/Organizations/Models/OrganizationSale.cs index c8ccb0f9a1..f1f3a636b7 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationSale.cs @@ -1,11 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; -namespace Bit.Core.Billing.Models.Sales; - -#nullable enable +namespace Bit.Core.Billing.Organizations.Models; public class OrganizationSale { diff --git a/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs b/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs similarity index 98% rename from src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs rename to src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs index 4bcc7ed699..ee603c67e0 100644 --- a/src/Core/Billing/Models/Business/SponsorOrganizationSubscriptionUpdate.cs +++ b/src/Core/Billing/Organizations/Models/SponsorOrganizationSubscriptionUpdate.cs @@ -4,7 +4,7 @@ using Bit.Core.Models.Business; using Stripe; -namespace Bit.Core.Billing.Models.Business; +namespace Bit.Core.Billing.Organizations.Models; public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate { diff --git a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/Billing/Organizations/Queries/GetCloudOrganizationLicenseQuery.cs similarity index 85% rename from src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs rename to src/Core/Billing/Organizations/Queries/GetCloudOrganizationLicenseQuery.cs index ff768ee03d..f00bc00356 100644 --- a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetCloudOrganizationLicenseQuery.cs @@ -3,8 +3,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -12,9 +11,15 @@ using Bit.Core.Models.Business; using Bit.Core.Platform.Installations; using Bit.Core.Services; -namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Billing.Organizations.Queries; -public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuery +public interface IGetCloudOrganizationLicenseQuery +{ + Task GetLicenseAsync(Organization organization, Guid installationId, + int? version = null); +} + +public class GetCloudOrganizationLicenseQuery : IGetCloudOrganizationLicenseQuery { private readonly IInstallationRepository _installationRepository; private readonly IPaymentService _paymentService; @@ -22,7 +27,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer private readonly IProviderRepository _providerRepository; private readonly IFeatureService _featureService; - public CloudGetOrganizationLicenseQuery( + public GetCloudOrganizationLicenseQuery( IInstallationRepository installationRepository, IPaymentService paymentService, ILicensingService licensingService, diff --git a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs b/src/Core/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQuery.cs similarity index 79% rename from src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs rename to src/Core/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQuery.cs index 8448554f24..ad6c2a2cdf 100644 --- a/src/Core/Billing/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQuery.cs @@ -2,8 +2,7 @@ #nullable disable using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -13,13 +12,18 @@ using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -namespace Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; +namespace Bit.Core.Billing.Organizations.Queries; -public class SelfHostedGetOrganizationLicenseQuery : BaseIdentityClientService, ISelfHostedGetOrganizationLicenseQuery +public interface IGetSelfHostedOrganizationLicenseQuery +{ + Task GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection); +} + +public class GetSelfHostedOrganizationLicenseQuery : BaseIdentityClientService, IGetSelfHostedOrganizationLicenseQuery { private readonly IGlobalSettings _globalSettings; - public SelfHostedGetOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger logger, ICurrentContext currentContext) + public GetSelfHostedOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger logger, ICurrentContext currentContext) : base( httpFactory, globalSettings.Installation.ApiUri, diff --git a/src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs b/src/Core/Billing/Organizations/Repositories/IOrganizationInstallationRepository.cs similarity index 74% rename from src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs rename to src/Core/Billing/Organizations/Repositories/IOrganizationInstallationRepository.cs index 05710d3966..cd96ab747e 100644 --- a/src/Core/Billing/Repositories/IOrganizationInstallationRepository.cs +++ b/src/Core/Billing/Organizations/Repositories/IOrganizationInstallationRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Organizations.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Organizations.Repositories; public interface IOrganizationInstallationRepository : IRepository { diff --git a/src/Core/Billing/Services/IOrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs similarity index 96% rename from src/Core/Billing/Services/IOrganizationBillingService.cs rename to src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs index 5f7d33f118..f35bafdd29 100644 --- a/src/Core/Billing/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs @@ -1,11 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Tax.Models; -namespace Bit.Core.Billing.Services; - -#nullable enable +namespace Bit.Core.Billing.Organizations.Services; public interface IOrganizationBillingService { diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs similarity index 99% rename from src/Core/Billing/Services/Implementations/OrganizationBillingService.cs rename to src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 725e274fa2..3fce618500 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -5,7 +5,9 @@ using Bit.Core.Billing.Enums; 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.Pricing; +using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; @@ -16,14 +18,11 @@ using Bit.Core.Settings; using Braintree; using Microsoft.Extensions.Logging; using Stripe; - using static Bit.Core.Billing.Utilities; using Customer = Stripe.Customer; using Subscription = Stripe.Subscription; -namespace Bit.Core.Billing.Services.Implementations; - -#nullable enable +namespace Bit.Core.Billing.Organizations.Services; public class OrganizationBillingService( IBraintreeGateway braintreeGateway, diff --git a/src/Core/Billing/Services/ILicensingService.cs b/src/Core/Billing/Services/ILicensingService.cs index 3b7ac2580d..b6ada998a7 100644 --- a/src/Core/Billing/Services/ILicensingService.cs +++ b/src/Core/Billing/Services/ILicensingService.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; diff --git a/src/Core/Billing/Services/Implementations/LicensingService.cs b/src/Core/Billing/Services/Implementations/LicensingService.cs index cdb330fe9b..3734f1747a 100644 --- a/src/Core/Billing/Services/Implementations/LicensingService.cs +++ b/src/Core/Billing/Services/Implementations/LicensingService.cs @@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Licenses.Models; using Bit.Core.Billing.Licenses.Services; using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Business; diff --git a/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs index 8f57d7b879..a54ba3546a 100644 --- a/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Settings; diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 284aaf7724..6e514bfea7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -8,9 +8,9 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; 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.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 846b9b94c8..1a16731305 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -8,7 +8,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs index a3e2063312..89c24ae6c6 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/OrganizationInstallationRepository.cs @@ -2,8 +2,8 @@ #nullable disable using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Organizations.Entities; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 00728ec1a0..35fc094973 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Providers.Repositories; -using Bit.Core.Billing.Repositories; using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Repositories; using Bit.Core.KeyManagement.Repositories; diff --git a/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs index da8ee8b1b6..4c3de7a899 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/OrganizationInstallation.cs @@ -7,7 +7,7 @@ using Bit.Infrastructure.EntityFramework.Platform; namespace Bit.Infrastructure.EntityFramework.Billing.Models; -public class OrganizationInstallation : Core.Billing.Entities.OrganizationInstallation +public class OrganizationInstallation : Core.Billing.Organizations.Entities.OrganizationInstallation { public virtual Installation Installation { get; set; } public virtual Organization Organization { get; set; } @@ -17,6 +17,6 @@ public class OrganizationInstallationMapperProfile : Profile { public OrganizationInstallationMapperProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs index 9656b1073d..c03df8d216 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/OrganizationInstallationRepository.cs @@ -2,8 +2,8 @@ #nullable disable using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Organizations.Entities; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index aab83ef7a8..7a6507230e 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Providers.Repositories; -using Bit.Core.Billing.Repositories; using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Dirt.Repositories; using Bit.Core.Enums; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs index a5d00339c1..078272d940 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationConnectionsControllerTests.cs @@ -4,7 +4,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index aff51b0d1d..51866320ee 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -2,7 +2,8 @@ using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Services; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs index 71bef89152..a776bbea22 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationsControllerTests.cs @@ -10,9 +10,9 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Billing.Organizations.Queries; +using Bit.Core.Billing.Organizations.Repositories; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -40,7 +40,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IPaymentService _paymentService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserService _userService; - private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; + private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery; private readonly ILicensingService _licensingService; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpgradeOrganizationPlanCommand _upgradeOrganizationPlanCommand; @@ -64,7 +64,7 @@ public class OrganizationsControllerTests : IDisposable _ssoConfigRepository = Substitute.For(); Substitute.For(); _userService = Substitute.For(); - _cloudGetOrganizationLicenseQuery = Substitute.For(); + _getCloudOrganizationLicenseQuery = Substitute.For(); _licensingService = Substitute.For(); _updateSecretsManagerSubscriptionCommand = Substitute.For(); _upgradeOrganizationPlanCommand = Substitute.For(); @@ -81,7 +81,7 @@ public class OrganizationsControllerTests : IDisposable _userService, _paymentService, _currentContext, - _cloudGetOrganizationLicenseQuery, + _getCloudOrganizationLicenseQuery, _globalSettings, _licensingService, _updateSecretsManagerSubscriptionCommand, diff --git a/test/Api.Test/Utilities/ApiHelpersTests.cs b/test/Api.Test/Utilities/ApiHelpersTests.cs index 771a4681bb..ec8f10ca6b 100644 --- a/test/Api.Test/Utilities/ApiHelpersTests.cs +++ b/test/Api.Test/Utilities/ApiHelpersTests.cs @@ -1,6 +1,6 @@ using System.Text; using Bit.Api.Utilities; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Microsoft.AspNetCore.Http; using NSubstitute; using Xunit; diff --git a/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs b/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs index fc11659fee..630ecd54bb 100644 --- a/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/SelfHostedOrganizationDetailsTests.cs @@ -5,7 +5,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs index 8acbe5eded..7e6f5dc9bc 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/CloudOrganizationSignUpCommandTests.cs @@ -1,9 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; 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.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs b/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs index bc0aeae98d..11c469e4ed 100644 --- a/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs +++ b/test/Core.Test/Billing/AutoFixture/OrganizationLicenseCustomization.cs @@ -1,5 +1,5 @@ using AutoFixture; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.Billing.AutoFixture; diff --git a/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs index 44ab82a04d..87bcd48187 100644 --- a/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseFileFixtures.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Enums; namespace Bit.Core.Test.Billing.Models.Business; diff --git a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs index 0be72a5374..e6767a5be5 100644 --- a/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Billing/Models/Business/OrganizationLicenseTests.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index a92ef72a13..a81b390ae1 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -2,7 +2,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -23,11 +23,11 @@ namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; [SubscriptionInfoCustomize] [OrganizationLicenseCustomize] [SutProviderCustomize] -public class CloudGetOrganizationLicenseQueryTests +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(); @@ -38,7 +38,7 @@ public class CloudGetOrganizationLicenseQueryTests [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; @@ -51,7 +51,7 @@ public class CloudGetOrganizationLicenseQueryTests [Theory] [BitAutoData] - public async Task GetLicenseAsync_CreatesAndReturns(SutProvider sutProvider, + public async Task GetLicenseAsync_CreatesAndReturns(SutProvider sutProvider, Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo, byte[] licenseSignature) { @@ -71,7 +71,7 @@ public class CloudGetOrganizationLicenseQueryTests [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 +90,7 @@ public class CloudGetOrganizationLicenseQueryTests [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) { diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs index 3fb960aa94..5d3d4acff7 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses.SelfHosted; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.OrganizationConnectionConfigs; @@ -15,12 +15,12 @@ using Xunit; namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; [SutProviderCustomize] -public class SelfHostedGetOrganizationLicenseQueryTests +public class GetSelfHostedOrganizationLicenseQueryTests { - private static SutProvider GetSutProvider(BillingSyncConfig config, + private static SutProvider GetSutProvider(BillingSyncConfig config, string apiResponse = null) { - return new SutProvider() + return new SutProvider() .ConfigureBaseIdentityClientService($"licenses/organization/{config.CloudOrganizationId}", HttpMethod.Get, apiResponse: apiResponse); } diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 4b11cddb35..3b69dc2dfc 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Billing.Organizations.Commands; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; diff --git a/test/Core.Test/Billing/Services/LicensingServiceTests.cs b/test/Core.Test/Billing/Services/LicensingServiceTests.cs index 1039f0bbfb..f33bda2164 100644 --- a/test/Core.Test/Billing/Services/LicensingServiceTests.cs +++ b/test/Core.Test/Billing/Services/LicensingServiceTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using AutoFixture; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Settings; using Bit.Core.Test.Billing.AutoFixture; diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index 26e6b98667..7edc60a26a 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Implementations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Utilities; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index e406915e38..1d39d63ef7 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -1,4 +1,5 @@ using AspNetCoreRateLimit; +using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; From 828003f1019ffd8edffd6d137b094ff3d877819a Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:44:20 -0400 Subject: [PATCH 064/326] [PM-19055] Add OTP Token Provider that is not dependent on the User entity (#6081) * feat(pm-19055) : - Add generic OTP generator. This OTP generator is not linked to .NET Identity giving us flexibility. - Update `OtpTokenProvider` to accept configuration object to keep interface clean. - Implement `OtpTokenProvider` in DI as open generic for flexibility. * test: 100% test coverage for `OtpTokenProvider` * doc: Added readme for `OtpTokenProvider` --- .../OtpTokenProvider/IOtpTokenProvider.cs | 29 ++ .../OtpTokenProvider/OtpTokenProvider.cs | 75 +++ .../OtpTokenProviderOptions.cs | 36 ++ .../TokenProviders/OtpTokenProvider/readme.md | 206 ++++++++ .../Utilities/ServiceCollectionExtensions.cs | 2 + .../Auth/Identity/OtpTokenProviderTests.cs | 459 ++++++++++++++++++ 6 files changed, 807 insertions(+) create mode 100644 src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/IOtpTokenProvider.cs create mode 100644 src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProvider.cs create mode 100644 src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProviderOptions.cs create mode 100644 src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/readme.md create mode 100644 test/Core.Test/Auth/Identity/OtpTokenProviderTests.cs diff --git a/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/IOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/IOtpTokenProvider.cs new file mode 100644 index 0000000000..bf153e80eb --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/IOtpTokenProvider.cs @@ -0,0 +1,29 @@ +namespace Bit.Core.Auth.Identity.TokenProviders; + +/// +/// A generic interface for a one-time password (OTP) token provider. +/// +public interface IOtpTokenProvider + where TOptions : DefaultOtpTokenProviderOptions +{ + /// + /// Generates a new one-time password (OTP) based on the configured parameters. + /// The generated OTP is stored in the distributed cache with a key based on the unique identifier and purpose. If the + /// key is already in use, it will overwrite and generate a new OTP with a refreshed TTL. + /// + /// Name of the token provider, used to distinguish different token providers that may inject this class + /// Purpose of the OTP token, used to distinguish different types of tokens. + /// Unique identifier to distinguish one request from another + /// generated token | null + Task GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier); + + /// + /// Validates the provided token against the stored value in the distributed cache. + /// + /// string value matched against the unique identifier in the cache if found + /// Name of the token provider, used to distinguish different token providers that may inject this class + /// Purpose of the OTP token, used to distinguish different types of tokens. + /// Unique identifier to distinguish one request from another + /// true if the token matches what is fetched from the cache, false if not. + Task ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier); +} diff --git a/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProvider.cs new file mode 100644 index 0000000000..b6280e13fe --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProvider.cs @@ -0,0 +1,75 @@ +using System.Text; +using Bit.Core.Utilities; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +public class OtpTokenProvider( + [FromKeyedServices("persistent")] + IDistributedCache distributedCache, + IOptions options) : IOtpTokenProvider + where TOptions : DefaultOtpTokenProviderOptions +{ + private readonly TOptions _otpTokenProviderOptions = options.Value; + + /// + /// This is where the OTP tokens are stored. + /// + private readonly IDistributedCache _distributedCache = distributedCache; + + /// + /// Used to store and fetch the OTP tokens from the distributed cache. + /// The format is "{tokenProviderName}_{purpose}_{uniqueIdentifier}". + /// + private readonly string _cacheKeyFormat = "{0}_{1}_{2}"; + + public async Task GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier) + { + if (string.IsNullOrEmpty(tokenProviderName) + || string.IsNullOrEmpty(purpose) + || string.IsNullOrEmpty(uniqueIdentifier)) + { + return null; + } + + var cacheKey = string.Format(_cacheKeyFormat, tokenProviderName, purpose, uniqueIdentifier); + var token = CoreHelpers.SecureRandomString( + _otpTokenProviderOptions.TokenLength, + _otpTokenProviderOptions.TokenAlpha, + true, + false, + _otpTokenProviderOptions.TokenNumeric, + false); + await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(token), _otpTokenProviderOptions.DistributedCacheEntryOptions); + return token; + } + + public async Task ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier) + { + if (string.IsNullOrEmpty(token) + || string.IsNullOrEmpty(tokenProviderName) + || string.IsNullOrEmpty(purpose) + || string.IsNullOrEmpty(uniqueIdentifier)) + { + return false; + } + + var cacheKey = string.Format(_cacheKeyFormat, tokenProviderName, purpose, uniqueIdentifier); + var cachedValue = await _distributedCache.GetAsync(cacheKey); + if (cachedValue == null) + { + return false; + } + + var code = Encoding.UTF8.GetString(cachedValue); + var valid = string.Equals(token, code); + if (valid) + { + await _distributedCache.RemoveAsync(cacheKey); + } + + return valid; + } +} diff --git a/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProviderOptions.cs b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProviderOptions.cs new file mode 100644 index 0000000000..95996d69a6 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/OtpTokenProviderOptions.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Caching.Distributed; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +/// +/// Options for configuring the OTP token provider. +/// +public class DefaultOtpTokenProviderOptions +{ + /// + /// Gets or sets the length of the generated token. + /// Default is 6 characters. + /// + public int TokenLength { get; set; } = 6; + + /// + /// Gets or sets whether the token should contain alphabetic characters. + /// Default is false. + /// + public bool TokenAlpha { get; set; } = false; + + /// + /// Gets or sets whether the token should contain numeric characters. + /// Default is true. + /// + public bool TokenNumeric { get; set; } = true; + + /// + /// Cache entry options for Otp Token provider. + /// Default is 5 minutes expiration. + /// + public DistributedCacheEntryOptions DistributedCacheEntryOptions { get; set; } = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; +} diff --git a/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/readme.md b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/readme.md new file mode 100644 index 0000000000..8cf12a98bf --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OtpTokenProvider/readme.md @@ -0,0 +1,206 @@ +# OtpTokenProvider + +The `OtpTokenProvider` is a token provider service for generating and validating Time-Based one-time passwords (TOTP). It provides a secure way to create temporary tokens for various authentication and verification scenarios. The provider can be configured to generate tokens specific to your use case by using the options pattern in the DI pipeline. + +## Overview + +The OTP Token Provider generates secure, time-limited tokens that can be used for: + +- Two-factor authentication +- Temporary access tokens for Sends +- Any scenario requiring short-lived verification codes + +## Features + +- **Configurable Token Length**: Default 6 characters, customizable +- **Character Set Options**: Numeric (default), alphabetic, or mixed +- **Distributed Caching**: Uses CosmosDb for cloud, or the configured database otherwise. +- **TTL Management**: Configurable expiration (default 5 minutes) +- **Secure Generation**: Uses cryptographically secure random generation +- **One-Time Use**: Tokens are automatically deleted from the cache after successful validation + +## Architecture + +### Interface: `IOtpTokenProvider` + +```csharp +public interface IOtpTokenProvider + where TOptions : DefaultOtpTokenProviderOptions +{ + Task GenerateTokenAsync(string tokenProviderName, string purpose, string uniqueIdentifier); + Task ValidateTokenAsync(string token, string tokenProviderName, string purpose, string uniqueIdentifier); +} +``` + +### Implementation: `OtpTokenProvider` + +The provider is initialized with: + +- **Distributed Cache**: Storage backend for tokens (using "persistent" keyed service) +- **IOptions**: Configuration options for token generation and caching + +## Usage + +### Basic Setup + +If your class needs the use the `IOtpTokenProvider` you can inject it like any other injectable class from the DI. + +### Generating a Token + +```csharp +// Generate a new OTP with token provider name, purpose and unique identifier +string token = await otpProvider.GenerateTokenAsync("EmailToken", "email_verification", $"{userId}_{securityStamp}"); +// Returns: "123456" (6-digit numeric by default) +``` + +### Validating a Token + +```csharp +// Validate user-provided token with same parameters used for generation +bool isValid = await otpProvider.ValidateTokenAsync("123456", "EmailToken", "email_verification", $"{userId}_{securityStamp}"); +// Returns: true if valid, false otherwise +// Note: Valid tokens are automatically removed from cache +``` + +### Custom Configurations + +If you need to modify the default options you can do so by creating an extension of the `DefaultOtpTokenProviderOptions` and using that class as the TOptions when injecting another IOtpTokenProvider service. + +#### OtpTokenProviderOptions + +```csharp +public class DefaultOtpTokenProviderOptions +{ ... } + +public class UserEmailOtpTokenOptions : DefaultOtpTokenProviderOptions { } +``` + +#### Service Collection + +```csharp +public static IdentityBuilder AddCustomIdentityServices( + this IServiceCollection services, GlobalSettings globalSettings) +{ + // possible customization + services.Configure(options => + { + options.TokenLength = 8; + // The other options are left default + }); + + // TryAddTransient open generics -> this allows us to inject IOtpTokenProvider without having to specify the specific type here. + services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>); +} +``` + +#### Usage + +```csharp +public class UserEmailTokenProvider( + IOtpTokenProvider otpTokenProvider +) +{ + private readonly IOtpTokenProvider _otpTokenProvider = otpTokenProvider; + ... +} +``` + +## Configuration Options + +### Token Properties + +| Property | Default | Description | +| -------------- | ------- | ---------------------------------------- | +| `TokenLength` | 6 | Number of characters in generated token | +| `TokenAlpha` | false | Include alphabetic characters (a-z, A-Z) | +| `TokenNumeric` | true | Include numeric characters (0-9) | + +### Cache Options + +See `DistributedCacheEntryOptions` documentation for a complete list of configuration options. + +| Property | Default | Description | +| --------------------------------- | --------- | ---------------------------- | +| `AbsoluteExpirationRelativeToNow` | 5 minutes | How long tokens remain valid | + +## Cache Key Format + +The cache key format uses three components: `{tokenProviderName}_{purpose}_{uniqueIdentifier}` + +### Examples: + +#### Possible Email Token Provider Example + +Email token provider uses: + +- **Token Provider Name**: `"EmailToken"` (identifies the specific use case) +- **Purpose**: `"EmailTwoFactorAuthentication"` (specific action being verified) +- **Unique Identifier**: `"{user.Id}_{securityStamp}"` (user-specific data) + +These are passed into the OTP Token Provider which creates a cache record: + +- Cache Key: `EmailToken_EmailTwoFactorAuthentication_guid_guid` + +## Security Considerations + +### Token Generation + +- Uses `CoreHelpers.SecureRandomString()` for cryptographically secure randomness +- No predictable patterns in generated tokens +- Configurable character sets for different security requirements + +### Storage + +- Tokens are stored in distributed cache. The cache depends on the specific deployment, for cloud it is CosmosDb. +- Automatic expiration prevents indefinite token validity +- One-time use prevents replay attacks + +### Validation + +- Exact string matching for validation +- Automatic removal after successful validation +- Returns `false` for expired or non-existent tokens + +## Dependency Injection + +The provider is registered in `ServiceCollectionExtensions.cs`: + +```csharp +services.TryAddScoped, OtpTokenProvider>(); +``` + +## Error Handling + +### Common Scenarios + +- **Token Not Found**: `ValidateTokenAsync()` returns `false` +- **Token Expired**: Automatically cleaned up by cache, validation returns `false` +- **Invalid Input**: + - `GenerateTokenAsync` returns `null` for empty/null tokenProviderName, purpose, or uniqueIdentifier + - `ValidateTokenAsync` returns `false` for empty/null token, tokenProviderName, purpose, or uniqueIdentifier + - No cache operations are performed for invalid inputs + +### Best Practices + +- Always check validation results +- Handle token expiration gracefully +- Provide clear user feedback for invalid tokens +- Implement rate limiting for token generation + +## Related Components + +- **`CoreHelpers.SecureRandomString()`**: Secure token generation +- **`IDistributedCache`**: Token storage backend +- **Two-Factor Authentication Providers**: Integration with 2FA flows +- **Email Services**: A Token delivery mechanism + +## Testing + +When testing components that use `OtpTokenProvider`: + +```csharp +// Mock the interface for unit tests +var mockOtpProvider = Substitute.For>(); +mockOtpProvider.GenerateTokenAsync("EmailToken", "email_verification", "user_123").Returns("123456"); +mockOtpProvider.ValidateTokenAsync("123456", "EmailToken", "email_verification", "user_123").Returns(true); +``` diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index dad0bc230e..0768aa7060 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -378,6 +378,8 @@ public static class ServiceCollectionExtensions public static IdentityBuilder AddCustomIdentityServices( this IServiceCollection services, GlobalSettings globalSettings) { + services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>)); + services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => diff --git a/test/Core.Test/Auth/Identity/OtpTokenProviderTests.cs b/test/Core.Test/Auth/Identity/OtpTokenProviderTests.cs new file mode 100644 index 0000000000..e19aa88f17 --- /dev/null +++ b/test/Core.Test/Auth/Identity/OtpTokenProviderTests.cs @@ -0,0 +1,459 @@ +using System.Text; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.Identity; + +[SutProviderCustomize] +public class OtpTokenProviderTests +{ + private readonly string _defaultTokenProviderName = "DefaultOtpProvider"; + + private readonly DefaultOtpTokenProviderOptions _defaultOtpTokenProviderOptions = new() + { + TokenLength = 6, + TokenAlpha = false, + TokenNumeric = true + }; + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_Success_ReturnsToken( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + sutProvider.GetDependency>() + .Value.Returns(_defaultOtpTokenProviderOptions); + sutProvider.Create(); + + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Equal(6, result.Length); // Default length + Assert.True(result.All(char.IsDigit)); // Default is numeric only + + // Verify cache was called with correct key + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + await sutProvider.GetDependency() + .Received(1) + .SetAsync(expectedCacheKey, Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_CustomConfiguration_ReturnsCorrectFormat( + SutProvider> sutProvider, + string tokenProviderName, + string purpose, + string uniqueIdentifier) + { + // Arrange + var otpConfig = new DefaultOtpTokenProviderOptions + { + TokenLength = 8, + TokenAlpha = true, + TokenNumeric = true + }; + + sutProvider.GetDependency>() + .Value.Returns(otpConfig); + sutProvider.Create(); + + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(tokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.NotNull(result); + Assert.Equal(8, result.Length); + Assert.Contains(result, char.IsLetterOrDigit); + } + + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_NumericOnly_ReturnsOnlyDigits( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + var otpConfig = new DefaultOtpTokenProviderOptions + { + TokenLength = 10, + TokenAlpha = false, + TokenNumeric = true + }; + + sutProvider.GetDependency>() + .Value.Returns(otpConfig); + sutProvider.Create(); + + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.Equal(10, result.Length); + Assert.True(result.All(char.IsDigit)); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_ValidToken_ReturnsTrue( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier, + string token) + { + // Arrange + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + var tokenBytes = Encoding.UTF8.GetBytes(token); + + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns(tokenBytes); + + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.True(result); + + // Verify token was removed from cache after successful validation + await sutProvider.GetDependency() + .Received(1) + .RemoveAsync(expectedCacheKey); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_InvalidToken_ReturnsFalse( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier, + string token, + string wrongToken) + { + // Arrange + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + var tokenBytes = Encoding.UTF8.GetBytes(wrongToken); // Different token in cache + + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns(tokenBytes); + + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.False(result); + + // Verify token was NOT removed from cache for invalid validation + await sutProvider.GetDependency() + .DidNotReceive() + .RemoveAsync(expectedCacheKey); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_TokenNotFound_ReturnsFalse( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier, + string token) + { + // Arrange + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns((byte[])null); // Token not found in cache + + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.False(result); + + // Verify removal was not attempted + await sutProvider.GetDependency() + .DidNotReceive() + .RemoveAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_EmptyToken_ReturnsFalse( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync("", _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_NullToken_ReturnsFalse( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(null, _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.False(result); + } + + // Tests for null/empty purpose and uniqueIdentifier parameters + [Theory, BitAutoData] + public async Task GenerateTokenAsync_NullPurpose_ReturnsNull( + SutProvider> sutProvider, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, null, uniqueIdentifier); + + // Assert + Assert.Null(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .SetAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_EmptyPurpose_ReturnsNull( + SutProvider> sutProvider, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, "", uniqueIdentifier); + + // Assert + Assert.Null(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .SetAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_NullUniqueIdentifier_ReturnsNull( + SutProvider> sutProvider, + string purpose) + { + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, null); + + // Assert + Assert.Null(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .SetAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_EmptyUniqueIdentifier_ReturnsNull( + SutProvider> sutProvider, + string purpose) + { + // Act + var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, ""); + + // Assert + Assert.Null(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .SetAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_NullPurpose_ReturnsFalse( + SutProvider> sutProvider, + string token, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, null, uniqueIdentifier); + + // Assert + Assert.False(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_EmptyPurpose_ReturnsFalse( + SutProvider> sutProvider, + string token, + string uniqueIdentifier) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, "", uniqueIdentifier); + + // Assert + Assert.False(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_NullUniqueIdentifier_ReturnsFalse( + SutProvider> sutProvider, + string token, + string purpose) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, null); + + // Assert + Assert.False(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_EmptyUniqueIdentifier_ReturnsFalse( + SutProvider> sutProvider, + string token, + string purpose) + { + // Act + var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, ""); + + // Assert + Assert.False(result); + + // Verify cache was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GenerateTokenAsync_OverwritesExistingToken( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + sutProvider.GetDependency>() + .Value.Returns(_defaultOtpTokenProviderOptions); + sutProvider.Create(); + + // Act - Generate token twice with same parameters + var firstToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + var secondToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.NotEqual(firstToken, secondToken); // Should be different tokens + + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + await sutProvider.GetDependency() + .Received(2) // Called twice - once for each generation + .SetAsync(expectedCacheKey, Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CacheKeyFormat_IsCorrect( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + sutProvider.GetDependency>() + .Value.Returns(_defaultOtpTokenProviderOptions); + sutProvider.Create(); + + // Act + await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + await sutProvider.GetDependency() + .Received(1) + .SetAsync(expectedCacheKey, Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateTokenAsync_CaseSensitive( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + var token = "ABC123"; + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + var tokenBytes = Encoding.UTF8.GetBytes(token); + + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns(tokenBytes); + + // Act & Assert + var validResult = await sutProvider.Sut.ValidateTokenAsync("ABC123", _defaultTokenProviderName, purpose, uniqueIdentifier); + Assert.True(validResult); + + // Reset the cache mock to return the token again + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns(tokenBytes); + + var invalidResult = await sutProvider.Sut.ValidateTokenAsync("abc123", _defaultTokenProviderName, purpose, uniqueIdentifier); + Assert.False(invalidResult); + } + + [Theory, BitAutoData] + public async Task RoundTrip_GenerateAndValidate_Success( + SutProvider> sutProvider, + string purpose, + string uniqueIdentifier) + { + // Arrange + sutProvider.GetDependency>() + .Value.Returns(_defaultOtpTokenProviderOptions); + sutProvider.Create(); + + var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; + byte[] storedToken = null; + + // Setup cache to capture stored token and return it on get + sutProvider.GetDependency() + .When(x => x.SetAsync(expectedCacheKey, Arg.Any(), Arg.Any())) + .Do(callInfo => storedToken = callInfo.ArgAt(1)); + + sutProvider.GetDependency() + .GetAsync(expectedCacheKey) + .Returns(callInfo => storedToken); + + // Act + var generatedToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); + var isValid = await sutProvider.Sut.ValidateTokenAsync(generatedToken, _defaultTokenProviderName, purpose, uniqueIdentifier); + + // Assert + Assert.True(isValid); + Assert.NotNull(generatedToken); + Assert.NotEmpty(generatedToken); + } +} From 30300bc59beae15d615dd12e3f4e6d3d3e1514bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:00:54 +0100 Subject: [PATCH 065/326] =?UTF-8?q?[PM-22103]=C2=A0Exclude=20default=20col?= =?UTF-8?q?lections=20from=20admin=20apis=20(#6021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: exclude DefaultUserCollection from GetManyByOrganizationIdWithPermissionsAsync Updated EF implementation, SQL procedure, and unit test to verify that default user collections are filtered from results * Update the public CollectionsController.Get method to return a NotFoundResult for collections of type DefaultUserCollection. * Add unit tests for the public CollectionsController * Update ICollectionRepository.GetManyByOrganizationIdAsync to exclude results of the type DefaultUserCollection Modified the SQL stored procedure and the EF query to reflect this change and added a new integration test to ensure the functionality works as expected. * Refactor CollectionsController to remove unused IApplicationCacheService dependency * Update IOrganizationUserRepository.GetDetailsByIdWithCollectionsAsync to exclude DefaultUserCollections * Update IOrganizationUserRepository.GetManyDetailsByOrganizationAsync to exclude DefaultUserCollections * Undo change to GetByIdWithCollectionsAsync * Update integration test to verify exclusion of DefaultUserCollection in OrganizationUserRepository.GetDetailsByIdWithCollectionsAsync * Clarify documentation in ICollectionRepository to specify that GetManyByOrganizationIdWithAccessAsync returns only shared collections belonging to the organization. * Add Arrange, Act, and Assert comments to CollectionsControllerTests --- .../Controllers/CollectionsController.cs | 9 +- .../IOrganizationUserRepository.cs | 12 ++ .../Repositories/ICollectionRepository.cs | 7 +- .../OrganizationUserRepository.cs | 5 +- .../Repositories/CollectionRepository.cs | 4 +- .../Queries/CollectionAdminDetailsQuery.cs | 7 +- ...llectionUser_ReadByOrganizationUserIds.sql | 4 + .../Collection_ReadByOrganizationId.sql | 3 +- ...serUserDetails_ReadWithCollectionsById.sql | 3 + ...on_ReadByOrganizationIdWithPermissions.sql | 3 +- .../Controllers/CollectionsControllerTests.cs | 122 ++++++++++++++ .../CollectionRepositoryTests.cs | 84 ++++++++++ .../OrganizationUserRepositoryTests.cs | 103 ++++++++++++ ...025-06-30_00_ExcludeDefaultCollections.sql | 149 ++++++++++++++++++ 14 files changed, 500 insertions(+), 15 deletions(-) create mode 100644 test/Api.Test/Public/Controllers/CollectionsControllerTests.cs create mode 100644 util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index 656f2980ca..836fe3a4f9 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -8,7 +8,6 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,18 +20,15 @@ public class CollectionsController : Controller private readonly ICollectionRepository _collectionRepository; private readonly IUpdateCollectionCommand _updateCollectionCommand; private readonly ICurrentContext _currentContext; - private readonly IApplicationCacheService _applicationCacheService; public CollectionsController( ICollectionRepository collectionRepository, IUpdateCollectionCommand updateCollectionCommand, - ICurrentContext currentContext, - IApplicationCacheService applicationCacheService) + ICurrentContext currentContext) { _collectionRepository = collectionRepository; _updateCollectionCommand = updateCollectionCommand; _currentContext = currentContext; - _applicationCacheService = applicationCacheService; } /// @@ -49,7 +45,8 @@ public class CollectionsController : Controller public async Task Get(Guid id) { (var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id); - if (collection == null || collection.OrganizationId != _currentContext.OrganizationId) + if (collection == null || collection.OrganizationId != _currentContext.OrganizationId || + collection.Type == CollectionType.DefaultUserCollection) { return new NotFoundResult(); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 6e07bd9ff8..cbdf3913cc 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -22,7 +22,19 @@ public interface IOrganizationUserRepository : IRepository GetByOrganizationAsync(Guid organizationId, Guid userId); Task>> GetByIdWithCollectionsAsync(Guid id); Task GetDetailsByIdAsync(Guid id); + /// + /// Returns the OrganizationUser and its associated collections (excluding DefaultUserCollections). + /// + /// The id of the OrganizationUser + /// A tuple containing the OrganizationUser and its associated collections Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id); + /// + /// Returns the OrganizationUsers and their associated collections (excluding DefaultUserCollections). + /// + /// The id of the organization + /// Whether to include groups + /// Whether to include collections + /// A list of OrganizationUserUserDetails Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 1d41a6ee1f..da4f6aa580 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -16,12 +16,12 @@ public interface ICollectionRepository : IRepository /// /// Return all collections that belong to the organization. Does not include any permission details or group/user - /// access relationships. + /// access relationships. Excludes default collections (My Items collections). /// Task> GetManyByOrganizationIdAsync(Guid organizationId); /// - /// Return all collections that belong to the organization. Includes group/user access relationships for each collection. + /// Return all shared collections that belong to the organization. Includes group/user access relationships for each collection. /// Task>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId); @@ -34,9 +34,10 @@ public interface ICollectionRepository : IRepository Task> GetManyByUserIdAsync(Guid userId); /// - /// Returns all collections for an organization, including permission info for the specified user. + /// Returns all shared collections for an organization, including permission info for the specified user. /// This does not perform any authorization checks internally! /// Optionally, you can include access relationships for other Groups/Users and the collections. + /// Excludes default collections (My Items collections) - used by Admin Console Collections tab. /// Task> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index f311baec90..bb392a2e60 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -257,7 +257,8 @@ public class OrganizationUserRepository : Repository new CollectionAccessSelection { @@ -369,6 +370,8 @@ public class OrganizationUserRepository : Repository c.OrganizationUserId).ToList(); } diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 73268d75bf..0840a3f751 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; @@ -216,7 +217,8 @@ public class CollectionRepository : Repository if (_organizationId.HasValue) { - baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId); + baseCollectionQuery = baseCollectionQuery.Where(x => + x.c.OrganizationId == _organizationId && + x.c.Type != CollectionType.DefaultUserCollection); } else if (_collectionId.HasValue) { diff --git a/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql index c78d7390a7..b3cf499f77 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql @@ -10,6 +10,10 @@ BEGIN [dbo].[OrganizationUser] OU INNER JOIN [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] INNER JOIN @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id] + WHERE + C.[Type] != 1 -- Exclude DefaultUserCollection END diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql index 0d317ebded..6a7fefeb6b 100644 --- a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql @@ -9,5 +9,6 @@ BEGIN FROM [dbo].[CollectionView] WHERE - [OrganizationId] = @OrganizationId + [OrganizationId] = @OrganizationId AND + [Type] != 1 -- Exclude DefaultUserCollection END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql index b76e4b8775..ed683d8392 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql @@ -15,6 +15,9 @@ BEGIN [dbo].[OrganizationUser] OU INNER JOIN [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] WHERE [OrganizationUserId] = @Id + AND C.[Type] != 1 -- Exclude default user collections END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql index 267024f56c..bd8d48b29b 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql @@ -66,7 +66,8 @@ BEGIN LEFT JOIN [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] WHERE - C.[OrganizationId] = @OrganizationId + C.[OrganizationId] = @OrganizationId AND + C.[Type] != 1 -- Exclude DefaultUserCollection GROUP BY C.[Id], C.[OrganizationId], diff --git a/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs new file mode 100644 index 0000000000..d896fc9c74 --- /dev/null +++ b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs @@ -0,0 +1,122 @@ +using Bit.Api.Models.Public.Response; +using Bit.Api.Public.Controllers; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Public.Controllers; + +[ControllerCustomize(typeof(CollectionsController))] +[SutProviderCustomize] +public class CollectionsControllerTests +{ + [Theory, BitAutoData] + public async Task Get_WithDefaultUserCollection_ReturnsNotFound( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + var access = new CollectionAccessDetails + { + Groups = new List(), + Users = new List() + }; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdWithAccessAsync(collection.Id) + .Returns(new Tuple(collection, access)); + + // Act + var result = await sutProvider.Sut.Get(collection.Id); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task Get_WithSharedCollection_ReturnsCollection( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.SharedCollection; + var access = new CollectionAccessDetails + { + Groups = [], + Users = [] + }; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdWithAccessAsync(collection.Id) + .Returns(new Tuple(collection, access)); + + // Act + var result = await sutProvider.Sut.Get(collection.Id); + + // Assert + var jsonResult = Assert.IsType(result); + var response = Assert.IsType(jsonResult.Value); + Assert.Equal(collection.Id, response.Id); + Assert.Equal(collection.Type, response.Type); + } + + [Theory, BitAutoData] + public async Task Delete_WithDefaultUserCollection_ReturnsBadRequest( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdAsync(collection.Id) + .Returns(collection); + + // Act + var result = await sutProvider.Sut.Delete(collection.Id); + + // Assert + var badRequestResult = Assert.IsType(result); + var errorResponse = Assert.IsType(badRequestResult.Value); + Assert.Contains("You cannot delete a collection with the type as DefaultUserCollection", errorResponse.Message); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task Delete_WithSharedCollection_ReturnsOk( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.SharedCollection; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdAsync(collection.Id) + .Returns(collection); + + // Act + var result = await sutProvider.Sut.Delete(collection.Id); + + // Assert + Assert.IsType(result); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(collection); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index b96998415d..90596a23b1 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -296,10 +296,29 @@ public class CollectionRepositoryTests } }, null); + // Create a default user collection (should be excluded from admin console results) + var defaultCollection = new Collection + { + Name = "My Items Collection", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }; + + await collectionRepository.CreateAsync(defaultCollection, null, users: new[] + { + new CollectionAccessSelection() + { + Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true + } + }); + var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); Assert.NotNull(collections); + // Should return only 3 collections (excluding the default user collection) + Assert.Equal(3, collections.Count); + collections = collections.OrderBy(c => c.Name).ToList(); Assert.Collection(collections, c1 => @@ -463,4 +482,69 @@ public class CollectionRepositoryTests Assert.False(c3.Unmanaged); }); } + + /// + /// Test to ensure collections are properly retrieved by organization + /// + [DatabaseTheory, DatabaseData] + public async Task GetManyByOrganizationIdAsync_Success( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IOrganizationUserRepository organizationUserRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection1, null, null); + + var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection2, null, null); + + var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection3, null, null); + + // Create a default user collection (should not be returned by this method) + var defaultCollection = new Collection + { + Name = "My Items", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }; + await collectionRepository.CreateAsync(defaultCollection, null, null); + + var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); + + Assert.NotNull(collections); + Assert.Equal(3, collections.Count); // Should only return the 3 shared collections, excluding the default user collection + Assert.All(collections, c => Assert.Equal(organization.Id, c.OrganizationId)); + Assert.All(collections, c => Assert.NotEqual(CollectionType.DefaultUserCollection, c.Type)); + + // Verify specific collections are returned + Assert.Contains(collections, c => c.Name == "Collection 1"); + Assert.Contains(collections, c => c.Name == "Collection 2"); + Assert.Contains(collections, c => c.Name == "Collection 3"); + Assert.DoesNotContain(collections, c => c.Name == "My Items"); + } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 0df5dcfb50..6919ce7bce 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -184,6 +184,87 @@ public class OrganizationUserRepositoryTests r.EncryptedPrivateKey == "privatekey"); } + [DatabaseTheory, DatabaseData] + public async Task GetManyDetailsByOrganizationAsync_WithIncludeCollections_ExcludesDefaultCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user.Email, + Plan = "Test", + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + // Create a regular collection + var regularCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Regular Collection", + Type = CollectionType.SharedCollection + }); + + // Create a default user collection + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Default Collection", + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = user.Email + }); + + // Assign the organization user to both collections + await organizationUserRepository.ReplaceAsync(orgUser, new List + { + new CollectionAccessSelection + { + Id = regularCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + }, + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + } + }); + + // Get organization users with collections included + var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync( + organization.Id, includeGroups: false, includeCollections: true); + + Assert.NotNull(organizationUsers); + Assert.Single(organizationUsers); + + var orgUserWithCollections = organizationUsers.First(); + Assert.NotNull(orgUserWithCollections.Collections); + + // Should only include the regular collection, not the default collection + Assert.Single(orgUserWithCollections.Collections); + Assert.Equal(regularCollection.Id, orgUserWithCollections.Collections.First().Id); + Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id); + } + [DatabaseTheory, DatabaseData] public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -498,6 +579,17 @@ public class OrganizationUserRepositoryTests RevisionDate = requestTime }); + // Create a default user collection that should be excluded from admin results + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "My Items", + Type = CollectionType.DefaultUserCollection, + CreationDate = requestTime, + RevisionDate = requestTime + }); + var group1 = await groupRepository.CreateAsync(new Group { Id = CoreHelpers.GenerateComb(), @@ -544,6 +636,13 @@ public class OrganizationUserRepositoryTests ReadOnly = true, HidePasswords = false, Manage = false + }, + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true } ], Groups = [group1.Id] @@ -605,7 +704,11 @@ public class OrganizationUserRepositoryTests var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id); var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id); Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id); + + // Should only return the regular collection, not the default collection (even though both were assigned) + Assert.Single(orgUser1.Collections); Assert.Equal(collection1.Id, orgUser1.Collections.First().Id); + Assert.DoesNotContain(orgUser1.Collections, c => c.Id == defaultCollection.Id); Assert.Equal(group1.Id, group1Database.First()); diff --git a/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql b/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql new file mode 100644 index 0000000000..d743f4e08c --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql @@ -0,0 +1,149 @@ +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationIdWithPermissions] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @IncludeAccessRelationships BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + MIN(CASE + WHEN + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 0 + ELSE 1 + END) AS [ReadOnly], + MIN(CASE + WHEN + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 0 + ELSE 1 + END) AS [HidePasswords], + MAX(CASE + WHEN + COALESCE(CU.[Manage], CG.[Manage], 0) = 0 + THEN 0 + ELSE 1 + END) AS [Manage], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) AS [Assigned], + CASE + WHEN + -- No user or group has manage rights + NOT EXISTS( + SELECT 1 + FROM [dbo].[CollectionUser] CU2 + JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id] + WHERE + CU2.[CollectionId] = C.[Id] AND + CU2.[Manage] = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionGroup] CG2 + WHERE + CG2.[CollectionId] = C.[Id] AND + CG2.[Manage] = 1 + ) + THEN 1 + ELSE 0 + END AS [Unmanaged] + FROM + [dbo].[CollectionView] C + LEFT JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + C.[OrganizationId] = @OrganizationId AND + C.[Type] != 1 -- Exclude DefaultUserCollection + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId], + C.[DefaultUserCollectionEmail], + C.[Type] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId + EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CollectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] != 1 -- Exclude DefaultUserCollection +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithCollectionsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [OrganizationUserUserDetails_ReadById] @Id + + SELECT + CU.[CollectionId] Id, + CU.[ReadOnly], + CU.[HidePasswords], + CU.[Manage] + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + WHERE + [OrganizationUserId] = @Id + AND C.[Type] != 1 -- Exclude default user collections +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CollectionUser_ReadByOrganizationUserIds] + @OrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + CU.* + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + INNER JOIN + @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id] + WHERE + C.[Type] != 1 -- Exclude DefaultUserCollection +END +GO From ae61150db5354803696b335637c7c51a0648f388 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:14:04 +0000 Subject: [PATCH 066/326] [deps] Tools: Update aws-sdk-net monorepo (#6106) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 7c2e6ef3e2..6f61b30bf4 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 79661dd5f5848df2ce6991a36bd3be06c20c1447 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:17:12 +0100 Subject: [PATCH 067/326] [PM 22967] Add change to enable organization after unlink (#6086) * Add change to enable organization after unlink * PM-22967 remove comments --- .../RemoveOrganizationFromProviderCommand.cs | 1 + ...oveOrganizationFromProviderCommandTests.cs | 64 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 824691d8d2..ed71b5f438 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -156,6 +156,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv organization.GatewaySubscriptionId = subscription.Id; organization.Status = OrganizationStatusType.Created; + organization.Enabled = true; await _providerBillingService.ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0); } 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 5be18116c0..c9b5b93d5e 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -263,7 +263,8 @@ public class RemoveOrganizationFromProviderCommandTests org => org.BillingEmail == "a@example.com" && org.GatewaySubscriptionId == "subscription_id" && - org.Status == OrganizationStatusType.Created)); + org.Status == OrganizationStatusType.Created && + org.Enabled == true)); // Verify organization is enabled when new subscription is created await sutProvider.GetDependency().Received(1) .DeleteAsync(providerOrganization); @@ -354,7 +355,8 @@ public class RemoveOrganizationFromProviderCommandTests org => org.BillingEmail == "a@example.com" && org.GatewaySubscriptionId == "subscription_id" && - org.Status == OrganizationStatusType.Created)); + org.Status == OrganizationStatusType.Created && + org.Enabled == true)); // Verify organization is enabled when new subscription is created await sutProvider.GetDependency().Received(1) .DeleteAsync(providerOrganization); @@ -390,4 +392,62 @@ public class RemoveOrganizationFromProviderCommandTests } } }; + + [Theory, BitAutoData] + public async Task RemoveOrganizationFromProvider_DisabledOrganization_ConsolidatedBilling_EnablesOrganization( + Provider provider, + ProviderOrganization providerOrganization, + Organization organization, + SutProvider sutProvider) + { + // Arrange: Set up a disabled organization that meets the criteria for consolidated billing + provider.Status = ProviderStatusType.Billable; + providerOrganization.ProviderId = provider.Id; + organization.Status = OrganizationStatusType.Managed; + organization.PlanType = PlanType.TeamsMonthly; + organization.Enabled = false; // Start with a disabled organization + + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); + + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + providerOrganization.OrganizationId, + [], + includeProvider: false) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + + organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([ + "owner@example.com" + ]); + + var stripeAdapter = sutProvider.GetDependency(); + + stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Any()) + .Returns(new Customer + { + Id = "customer_id", + Address = new Address + { + Country = "US" + } + }); + + stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription + { + Id = "new_subscription_id" + }); + + // Act + await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); + + // Assert: Verify the disabled organization is now enabled + await organizationRepository.Received(1).ReplaceAsync(Arg.Is( + org => + org.Enabled == true && // The previously disabled organization should now be enabled + org.Status == OrganizationStatusType.Created && + org.GatewaySubscriptionId == "new_subscription_id")); + } } From 4464bfe900a7669733f8ae5380d204c0422c6e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 21 Jul 2025 14:35:41 +0100 Subject: [PATCH 068/326] [PM-15159] Create SelfHostedOrganizationSignUp command (#6089) * Add SelfHostedOrganizationSignUpCommand for organization sign-up process Method extracted from OrganizationService * Register SelfHostedOrganizationSignUpCommand for dependency injection * Add unit tests for SelfHostedOrganizationSignUpCommand * Refactor SelfHostedOrganizationLicensesController to use ISelfHostedOrganizationSignUpCommand * Remove SignUpAsync method and related validation from IOrganizationService and OrganizationService * Move ISelfHostedOrganizationSignUpCommand into a separate file and update references * Enable null safety in SelfHostedOrganizationSignUpCommand and update ISelfHostedOrganizationSignUpCommand interface to reflect nullable types for organizationUser and collectionName. --- ...elfHostedOrganizationLicensesController.cs | 9 +- .../ISelfHostedOrganizationSignUpCommand.cs | 15 + .../SelfHostedOrganizationSignUpCommand.cs | 216 +++++++++++ .../Services/IOrganizationService.cs | 6 - .../Implementations/OrganizationService.cs | 158 -------- ...OrganizationServiceCollectionExtensions.cs | 1 + ...elfHostedOrganizationSignUpCommandTests.cs | 351 ++++++++++++++++++ 7 files changed, 588 insertions(+), 168 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index d0411f9bc5..b4eecdba0f 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -5,6 +5,7 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Organizations; using Bit.Api.Utilities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Organizations.Queries; @@ -28,7 +29,7 @@ public class SelfHostedOrganizationLicensesController : Controller private readonly ICurrentContext _currentContext; private readonly IGetSelfHostedOrganizationLicenseQuery _getSelfHostedOrganizationLicenseQuery; private readonly IOrganizationConnectionRepository _organizationConnectionRepository; - private readonly IOrganizationService _organizationService; + private readonly ISelfHostedOrganizationSignUpCommand _selfHostedOrganizationSignUpCommand; private readonly IOrganizationRepository _organizationRepository; private readonly IUserService _userService; private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand; @@ -37,7 +38,7 @@ public class SelfHostedOrganizationLicensesController : Controller ICurrentContext currentContext, IGetSelfHostedOrganizationLicenseQuery getSelfHostedOrganizationLicenseQuery, IOrganizationConnectionRepository organizationConnectionRepository, - IOrganizationService organizationService, + ISelfHostedOrganizationSignUpCommand selfHostedOrganizationSignUpCommand, IOrganizationRepository organizationRepository, IUserService userService, IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand) @@ -45,7 +46,7 @@ public class SelfHostedOrganizationLicensesController : Controller _currentContext = currentContext; _getSelfHostedOrganizationLicenseQuery = getSelfHostedOrganizationLicenseQuery; _organizationConnectionRepository = organizationConnectionRepository; - _organizationService = organizationService; + _selfHostedOrganizationSignUpCommand = selfHostedOrganizationSignUpCommand; _organizationRepository = organizationRepository; _userService = userService; _updateOrganizationLicenseCommand = updateOrganizationLicenseCommand; @@ -66,7 +67,7 @@ public class SelfHostedOrganizationLicensesController : Controller throw new BadRequestException("Invalid license"); } - var result = await _organizationService.SignUpAsync(license, user, model.Key, + var result = await _selfHostedOrganizationSignUpCommand.SignUpAsync(license, user, model.Key, model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); return new OrganizationResponseModel(result.Item1, null); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..2686384a34 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/ISelfHostedOrganizationSignUpCommand.cs @@ -0,0 +1,15 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface ISelfHostedOrganizationSignUpCommand +{ + /// + /// Create a new organization on a self-hosted instance + /// + Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync( + OrganizationLicense license, User owner, string ownerKey, + string? collectionName, string publicKey, string privateKey); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..c52b7c10c9 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommand.cs @@ -0,0 +1,216 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class SelfHostedOrganizationSignUpCommand : ISelfHostedOrganizationSignUpCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly IApplicationCacheService _applicationCacheService; + private readonly ICollectionRepository _collectionRepository; + private readonly IPushRegistrationService _pushRegistrationService; + private readonly IPushNotificationService _pushNotificationService; + private readonly IDeviceRepository _deviceRepository; + private readonly ILicensingService _licensingService; + private readonly IPolicyService _policyService; + private readonly IGlobalSettings _globalSettings; + private readonly IPaymentService _paymentService; + + public SelfHostedOrganizationSignUpCommand( + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + ICollectionRepository collectionRepository, + IPushRegistrationService pushRegistrationService, + IPushNotificationService pushNotificationService, + IDeviceRepository deviceRepository, + ILicensingService licensingService, + IPolicyService policyService, + IGlobalSettings globalSettings, + IPaymentService paymentService) + { + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; + _applicationCacheService = applicationCacheService; + _collectionRepository = collectionRepository; + _pushRegistrationService = pushRegistrationService; + _pushNotificationService = pushNotificationService; + _deviceRepository = deviceRepository; + _licensingService = licensingService; + _policyService = policyService; + _globalSettings = globalSettings; + _paymentService = paymentService; + } + + public async Task<(Organization organization, OrganizationUser? organizationUser)> SignUpAsync( + OrganizationLicense license, User owner, string ownerKey, string? collectionName, string publicKey, + string privateKey) + { + if (license.LicenseType != LicenseType.Organization) + { + throw new BadRequestException("Premium licenses cannot be applied to an organization. " + + "Upload this license from your personal account settings page."); + } + + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception); + + if (!canUse) + { + throw new BadRequestException(exception); + } + + var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); + if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey))) + { + throw new BadRequestException("License is already in use by another organization."); + } + + await ValidateSignUpPoliciesAsync(owner.Id); + + var organization = claimsPrincipal != null + // If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization. + ? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey) + // If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization. + : OrganizationFactory.Create(owner, license, publicKey, privateKey); + + var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); + + var dir = $"{_globalSettings.LicenseDirectory}/organization"; + Directory.CreateDirectory(dir); + await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create); + await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented); + return (result.organization, result.organizationUser); + } + + private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + { + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + /// + /// Private helper method to create a new organization. + /// This is common code used by both the cloud and self-hosted methods. + /// + private async Task<(Organization organization, OrganizationUser? organizationUser, Collection? defaultCollection)> + SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, string? collectionName, bool withPayment) + { + try + { + await _organizationRepository.CreateAsync(organization); + await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + // ownerId == default if the org is created by a provider - in this case it's created without an + // owner and the first owner is immediately invited afterwards + OrganizationUser? orgUser = null; + if (ownerId != default) + { + orgUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = ownerId, + Key = ownerKey, + AccessSecretsManager = organization.UseSecretsManager, + Type = OrganizationUserType.Owner, + Status = OrganizationUserStatusType.Confirmed, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + orgUser.SetNewId(); + + await _organizationUserRepository.CreateAsync(orgUser); + + var devices = await GetUserDeviceIdsAsync(orgUser.UserId!.Value); + await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, + organization.Id.ToString()); + await _pushNotificationService.PushSyncOrgKeysAsync(ownerId); + } + + Collection? defaultCollection = null; + if (!string.IsNullOrWhiteSpace(collectionName)) + { + defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + + // Give the owner Can Manage access over the default collection + List? defaultOwnerAccess = null; + if (orgUser != null) + { + defaultOwnerAccess = + [ + new CollectionAccessSelection + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + } + ]; + } + + await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + + return (organization, orgUser, defaultCollection); + } + catch + { + if (withPayment) + { + await _paymentService.CancelAndRecoverChargesAsync(organization); + } + + if (organization.Id != default(Guid)) + { + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } + + private async Task> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + return devices + .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) + .Select(d => d.Id.ToString()); + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 2fa6772c62..bec9507adf 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; -using Bit.Core.Billing.Organizations.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; @@ -21,11 +20,6 @@ public interface IOrganizationService Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); - /// - /// Create a new organization on a self-hosted instance - /// - Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, - string ownerKey, string collectionName, string publicKey, string privateKey); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 3e81494fc3..4f25f5fc53 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -20,7 +20,6 @@ using Bit.Core.Auth.Repositories; 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.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -396,155 +395,6 @@ public class OrganizationService : IOrganizationService } } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) - { - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - throw new BadRequestException("You may not create an organization. You belong to an organization " + - "which has a policy that prohibits you from being a member of any other organization."); - } - } - - /// - /// Create a new organization on a self-hosted instance - /// - public async Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync( - OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, - string privateKey) - { - if (license.LicenseType != LicenseType.Organization) - { - throw new BadRequestException("Premium licenses cannot be applied to an organization. " + - "Upload this license from your personal account settings page."); - } - - var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); - var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception); - - if (!canUse) - { - throw new BadRequestException(exception); - } - - var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync(); - if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey))) - { - throw new BadRequestException("License is already in use by another organization."); - } - - await ValidateSignUpPoliciesAsync(owner.Id); - - var organization = claimsPrincipal != null - // If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization. - ? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey) - // If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization. - : OrganizationFactory.Create(owner, license, publicKey, privateKey); - - var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); - - var dir = $"{_globalSettings.LicenseDirectory}/organization"; - Directory.CreateDirectory(dir); - await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create); - await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented); - return (result.organization, result.organizationUser); - } - - /// - /// Private helper method to create a new organization. - /// This is common code used by both the cloud and self-hosted methods. - /// - private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> - SignUpAsync(Organization organization, - Guid ownerId, string ownerKey, string collectionName, bool withPayment) - { - try - { - await _organizationRepository.CreateAsync(organization); - await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey - { - OrganizationId = organization.Id, - ApiKey = CoreHelpers.SecureRandomString(30), - Type = OrganizationApiKeyType.Default, - RevisionDate = DateTime.UtcNow, - }); - await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); - - // ownerId == default if the org is created by a provider - in this case it's created without an - // owner and the first owner is immediately invited afterwards - OrganizationUser orgUser = null; - if (ownerId != default) - { - orgUser = new OrganizationUser - { - OrganizationId = organization.Id, - UserId = ownerId, - Key = ownerKey, - AccessSecretsManager = organization.UseSecretsManager, - Type = OrganizationUserType.Owner, - Status = OrganizationUserStatusType.Confirmed, - CreationDate = organization.CreationDate, - RevisionDate = organization.CreationDate - }; - orgUser.SetNewId(); - - await _organizationUserRepository.CreateAsync(orgUser); - - var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value); - await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices, - organization.Id.ToString()); - await _pushNotificationService.PushSyncOrgKeysAsync(ownerId); - } - - Collection defaultCollection = null; - if (!string.IsNullOrWhiteSpace(collectionName)) - { - defaultCollection = new Collection - { - Name = collectionName, - OrganizationId = organization.Id, - CreationDate = organization.CreationDate, - RevisionDate = organization.CreationDate - }; - - // Give the owner Can Manage access over the default collection - List defaultOwnerAccess = null; - if (orgUser != null) - { - defaultOwnerAccess = - [ - new CollectionAccessSelection - { - Id = orgUser.Id, - HidePasswords = false, - ReadOnly = false, - Manage = true - } - ]; - } - - await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); - } - - return (organization, orgUser, defaultCollection); - } - catch - { - if (withPayment) - { - await _paymentService.CancelAndRecoverChargesAsync(organization); - } - - if (organization.Id != default(Guid)) - { - await _organizationRepository.DeleteAsync(organization); - await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); - } - - throw; - } - } - public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); @@ -1338,14 +1188,6 @@ public class OrganizationService : IOrganizationService await _groupRepository.UpdateUsersAsync(group.Id, users); } - private async Task> GetUserDeviceIdsAsync(Guid userId) - { - var devices = await _deviceRepository.GetManyByUserIdAsync(userId); - return devices - .Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) - .Select(d => d.Id.ToString()); - } - public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null) { await _organizationRepository.ReplaceAsync(org); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index ae24017e48..b78a305d31 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationDeleteCommands(this IServiceCollection services) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..26c092797b --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/SelfHostedOrganizationSignUpCommandTests.cs @@ -0,0 +1,351 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class SelfHostedOrganizationSignUpCommandTests +{ + [Theory, BitAutoData] + public async Task SignUpAsync_WithValidRequest_CreatesOrganizationSuccessfully( + User owner, string ownerKey, string collectionName, string publicKey, + string privateKey, List devices, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(owner.Id) + .Returns(devices); + + // Act + var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey); + + // Assert + Assert.NotNull(result.organization); + Assert.NotNull(result.organizationUser); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(result.organization); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(key => + key.OrganizationId == result.organization.Id && + key.Type == OrganizationApiKeyType.Default && + !string.IsNullOrEmpty(key.ApiKey))); + + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(result.organization); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(user => + user.OrganizationId == result.organization.Id && + user.UserId == owner.Id && + user.Key == ownerKey && + user.Type == OrganizationUserType.Owner && + user.Status == OrganizationUserStatusType.Confirmed)); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(c => c.Name == collectionName && c.OrganizationId == result.organization.Id), + Arg.Is>(groups => groups == null), + Arg.Is>(access => + access.Any(a => a.Id == result.organizationUser.Id && a.Manage && !a.ReadOnly && !a.HidePasswords))); + + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(owner.Id); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithPremiumLicense_ThrowsBadRequestException( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings, LicenseType.User); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey)); + + Assert.Contains("Premium licenses cannot be applied to an organization", exception.Message); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithInvalidLicense_ThrowsBadRequestException( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + license.CanUse(globalSettings, sutProvider.GetDependency(), null, out _) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey)); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithLicenseAlreadyInUse_ThrowsBadRequestException( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, Organization existingOrganization, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + existingOrganization.LicenseKey = license.LicenseKey; + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .GetManyByEnabledAsync() + .Returns(new List { existingOrganization }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey)); + + Assert.Contains("License is already in use", exception.Message); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithSingleOrgPolicy_ThrowsBadRequestException( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey)); + + Assert.Contains("You may not create an organization", exception.Message); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithClaimsPrincipal_UsesClaimsPrincipalToCreateOrganization( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, ClaimsPrincipal claimsPrincipal, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns(claimsPrincipal); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(owner.Id) + .Returns(new List()); + + // Act + var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey); + + // Assert + Assert.NotNull(result.organization); + Assert.NotNull(result.organizationUser); + + sutProvider.GetDependency() + .Received(1) + .GetClaimsPrincipalFromLicense(license); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithoutCollectionName_DoesNotCreateCollection( + User owner, string ownerKey, string publicKey, string privateKey, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(owner.Id) + .Returns(new List()); + + // Act + var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, null, publicKey, privateKey); + + // Assert + Assert.NotNull(result.organization); + Assert.NotNull(result.organizationUser); + + await sutProvider.GetDependency() + .DidNotReceive() + .CreateAsync(Arg.Any(), Arg.Is>(x => true), Arg.Is>(x => true)); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_WithDevices_RegistersDevicesForPushNotifications( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, List devices, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + foreach (var device in devices) + { + device.PushToken = "push-token-" + device.Id; + } + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(owner.Id) + .Returns(devices); + + // Act + var result = await sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey); + + // Assert + Assert.NotNull(result.organization); + Assert.NotNull(result.organizationUser); + + var expectedDeviceIds = devices.Select(d => d.Id.ToString()); + await sutProvider.GetDependency() + .Received(1) + .AddUserRegistrationOrganizationAsync( + Arg.Is>(ids => ids.SequenceEqual(expectedDeviceIds)), + result.organization.Id.ToString()); + } + + [Theory, BitAutoData] + public async Task SignUpAsync_OnException_CleansUpOrganization( + User owner, string ownerKey, string collectionName, + string publicKey, string privateKey, + SutProvider sutProvider) + { + // Arrange + var globalSettings = sutProvider.GetDependency(); + var license = CreateValidOrganizationLicense(globalSettings); + + SetupCommonMocks(sutProvider, owner); + SetupLicenseValidation(sutProvider, license); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Throws(new Exception("Test exception")); + + // Act & Assert + await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpAsync(license, owner, ownerKey, collectionName, publicKey, privateKey)); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Any()); + + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(Arg.Any()); + } + + private void SetupCommonMocks( + SutProvider sutProvider, + User owner) + { + var globalSettings = sutProvider.GetDependency(); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(callInfo => + { + var org = callInfo.Arg(); + org.Id = Guid.NewGuid(); + return Task.FromResult(org); + }); + + sutProvider.GetDependency() + .AnyPoliciesApplicableToUserAsync(owner.Id, PolicyType.SingleOrg) + .Returns(false); + + globalSettings.LicenseDirectory.Returns("/tmp/licenses"); + } + + private void SetupLicenseValidation( + SutProvider sutProvider, + OrganizationLicense license) + { + var globalSettings = sutProvider.GetDependency(); + + sutProvider.GetDependency() + .VerifyLicense(license) + .Returns(true); + + license.CanUse(globalSettings, sutProvider.GetDependency(), null, out _) + .Returns(true); + } + + private OrganizationLicense CreateValidOrganizationLicense( + IGlobalSettings globalSettings, + LicenseType licenseType = LicenseType.Organization) + { + return new OrganizationLicense + { + LicenseType = licenseType, + Signature = Guid.NewGuid().ToString().Replace('-', '+'), + Issued = DateTime.UtcNow.AddDays(-1), + Expires = DateTime.UtcNow.AddDays(10), + Version = OrganizationLicense.CurrentLicenseFileVersion, + InstallationId = globalSettings.Installation.Id, + Enabled = true, + SelfHost = true + }; + } +} From b0b2b94fc9e2d891e373b6cff7a37b788d1b1c86 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 21 Jul 2025 11:54:00 -0400 Subject: [PATCH 069/326] Remove X509ChainCustomization Feature (#6108) * Remove X509ChainCustomization Feature * `dotnet format` --- .../PostConfigureX509ChainOptions.cs | 96 ----- ...ustomizationServiceCollectionExtensions.cs | 53 --- .../X509ChainOptions.cs | 81 ---- .../MailKitSmtpMailDeliveryService.cs | 15 +- .../MailKitSmtpMailDeliveryServiceTests.cs | 128 +------ ...izationServiceCollectionExtensionsTests.cs | 359 ------------------ .../Services/HandlebarsMailServiceTests.cs | 4 +- .../MailKitSmtpMailDeliveryServiceTests.cs | 7 +- 8 files changed, 9 insertions(+), 734 deletions(-) delete mode 100644 src/Core/Platform/X509ChainCustomization/PostConfigureX509ChainOptions.cs delete mode 100644 src/Core/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensions.cs delete mode 100644 src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs delete mode 100644 test/Core.Test/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensionsTests.cs diff --git a/src/Core/Platform/X509ChainCustomization/PostConfigureX509ChainOptions.cs b/src/Core/Platform/X509ChainCustomization/PostConfigureX509ChainOptions.cs deleted file mode 100644 index 963294e85f..0000000000 --- a/src/Core/Platform/X509ChainCustomization/PostConfigureX509ChainOptions.cs +++ /dev/null @@ -1,96 +0,0 @@ -#nullable enable - -using System.Security.Cryptography.X509Certificates; -using Bit.Core.Settings; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Bit.Core.Platform.X509ChainCustomization; - -internal sealed class PostConfigureX509ChainOptions : IPostConfigureOptions -{ - const string CertificateSearchPattern = "*.crt"; - - private readonly ILogger _logger; - private readonly IHostEnvironment _hostEnvironment; - private readonly GlobalSettings _globalSettings; - - public PostConfigureX509ChainOptions( - ILogger logger, - IHostEnvironment hostEnvironment, - GlobalSettings globalSettings) - { - _logger = logger; - _hostEnvironment = hostEnvironment; - _globalSettings = globalSettings; - } - - public void PostConfigure(string? name, X509ChainOptions options) - { - // We don't register or request a named instance of these options, - // so don't customize it. - if (name != Options.DefaultName) - { - return; - } - - // We only allow this setting to be configured on self host. - if (!_globalSettings.SelfHosted) - { - options.AdditionalCustomTrustCertificatesDirectory = null; - return; - } - - if (options.AdditionalCustomTrustCertificates != null) - { - // Additional certificates were added directly, this overwrites the need to - // read them from the directory. - _logger.LogInformation( - "Additional custom trust certificates were added directly, skipping loading them from '{Directory}'", - options.AdditionalCustomTrustCertificatesDirectory - ); - return; - } - - if (string.IsNullOrEmpty(options.AdditionalCustomTrustCertificatesDirectory)) - { - return; - } - - if (!Directory.Exists(options.AdditionalCustomTrustCertificatesDirectory)) - { - // The default directory is volume mounted via the default Bitwarden setup process. - // If the directory doesn't exist it could indicate a error in configuration but this - // directory is never expected in a normal development environment so lower the log - // level in that case. - var logLevel = _hostEnvironment.IsDevelopment() - ? LogLevel.Debug - : LogLevel.Warning; - _logger.Log( - logLevel, - "An additional custom trust certificate directory was given '{Directory}' but that directory does not exist.", - options.AdditionalCustomTrustCertificatesDirectory - ); - return; - } - - var certificates = new List(); - - foreach (var certFile in Directory.EnumerateFiles(options.AdditionalCustomTrustCertificatesDirectory, CertificateSearchPattern)) - { - certificates.Add(new X509Certificate2(certFile)); - } - - if (options.AdditionalCustomTrustCertificatesDirectory != X509ChainOptions.DefaultAdditionalCustomTrustCertificatesDirectory && certificates.Count == 0) - { - // They have intentionally given us a non-default directory but there weren't certificates, that is odd. - _logger.LogWarning( - "No additional custom trust certificates were found in '{Directory}'", - options.AdditionalCustomTrustCertificatesDirectory - ); - } - - options.AdditionalCustomTrustCertificates = certificates; - } -} diff --git a/src/Core/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensions.cs b/src/Core/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensions.cs deleted file mode 100644 index 46bd5b37e6..0000000000 --- a/src/Core/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Bit.Core.Platform.X509ChainCustomization; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for setting up the ability to provide customization to how X509 chain validation works in an . -/// -public static class X509ChainCustomizationServiceCollectionExtensions -{ - /// - /// Configures X509ChainPolicy customization through the root level X509ChainOptions configuration section - /// and configures the primary to use custom certificate validation - /// when customized to do so. - /// - /// The . - /// The for additional chaining. - public static IServiceCollection AddX509ChainCustomization(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - services.AddOptions() - .BindConfiguration(nameof(X509ChainOptions)); - - // Use TryAddEnumerable to make sure `PostConfigureX509ChainOptions` isn't added multiple - // times even if this method is called multiple times. - services.TryAddEnumerable(ServiceDescriptor.Singleton, PostConfigureX509ChainOptions>()); - - services.AddHttpClient() - .ConfigureHttpClientDefaults(builder => - { - builder.ConfigurePrimaryHttpMessageHandler(sp => - { - var x509ChainOptions = sp.GetRequiredService>().Value; - - var handler = new HttpClientHandler(); - - if (x509ChainOptions.TryGetCustomRemoteCertificateValidationCallback(out var callback)) - { - handler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, errors) => - { - return callback(certificate, chain, errors); - }; - } - - return handler; - }); - }); - - return services; - } -} diff --git a/src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs b/src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs deleted file mode 100644 index 6cd06acf3c..0000000000 --- a/src/Core/Platform/X509ChainCustomization/X509ChainOptions.cs +++ /dev/null @@ -1,81 +0,0 @@ -#nullable enable - -using System.Diagnostics.CodeAnalysis; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - -namespace Bit.Core.Platform.X509ChainCustomization; - -/// -/// Allows for customization of the and access to a custom server certificate validator -/// if customization has been made. -/// -public sealed class X509ChainOptions -{ - // This is the directory that we historically used to allow certificates be added inside our container - // and then on start of the container we would move them to `/usr/local/share/ca-certificates/` and call - // `update-ca-certificates` but since that operation requires root we can't do it in a rootless container. - // Ref: https://github.com/bitwarden/server/blob/67d7d685a619a5fc413f8532dacb09681ee5c956/src/Api/entrypoint.sh#L38-L41 - public const string DefaultAdditionalCustomTrustCertificatesDirectory = "/etc/bitwarden/ca-certificates/"; - - /// - /// A directory where additional certificates should be read from and included in . - /// - /// - /// Only certificates suffixed with *.crt will be read. If is - /// set, then this directory will not be read from. - /// - public string? AdditionalCustomTrustCertificatesDirectory { get; set; } = DefaultAdditionalCustomTrustCertificatesDirectory; - - /// - /// A list of additional certificates that should be included in . - /// - /// - /// If this value is set manually, then will be ignored. - /// - public List? AdditionalCustomTrustCertificates { get; set; } - - /// - /// Attempts to retrieve a custom remote certificate validation callback. - /// - /// - /// Returns when we have custom remote certification that should be added, - /// when no custom validation is needed and the default validation callback should - /// be used instead. - /// - [MemberNotNullWhen(true, nameof(AdditionalCustomTrustCertificates))] - public bool TryGetCustomRemoteCertificateValidationCallback( - [MaybeNullWhen(false)] out Func callback) - { - callback = null; - if (AdditionalCustomTrustCertificates == null || AdditionalCustomTrustCertificates.Count == 0) - { - return false; - } - - // Do this outside of the callback so that we aren't opening the root store every request. - using var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine, OpenFlags.ReadOnly); - var rootCertificates = store.Certificates; - - // Ref: https://github.com/dotnet/runtime/issues/39835#issuecomment-663020581 - callback = (certificate, chain, errors) => - { - if (chain == null || certificate == null) - { - return false; - } - - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - - // We want our additional certificates to be in addition to the machines root store. - chain.ChainPolicy.CustomTrustStore.AddRange(rootCertificates); - - foreach (var additionalCertificate in AdditionalCustomTrustCertificates) - { - chain.ChainPolicy.CustomTrustStore.Add(additionalCertificate); - } - return chain.Build(certificate); - }; - return true; - } -} diff --git a/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs b/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs index f12714e462..04eda42d22 100644 --- a/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs +++ b/src/Core/Services/Implementations/MailKitSmtpMailDeliveryService.cs @@ -1,13 +1,10 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using System.Security.Cryptography.X509Certificates; -using Bit.Core.Platform.X509ChainCustomization; using Bit.Core.Settings; using Bit.Core.Utilities; using MailKit.Net.Smtp; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using MimeKit; namespace Bit.Core.Services; @@ -16,14 +13,12 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService { private readonly GlobalSettings _globalSettings; private readonly ILogger _logger; - private readonly X509ChainOptions _x509ChainOptions; private readonly string _replyDomain; private readonly string _replyEmail; public MailKitSmtpMailDeliveryService( GlobalSettings globalSettings, - ILogger logger, - IOptions x509ChainOptions) + ILogger logger) { if (globalSettings.Mail.Smtp?.Host == null) { @@ -44,7 +39,6 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService _globalSettings = globalSettings; _logger = logger; - _x509ChainOptions = x509ChainOptions.Value; } public async Task SendEmailAsync(Models.Mail.MailMessage message) @@ -89,13 +83,6 @@ public class MailKitSmtpMailDeliveryService : IMailDeliveryService { client.ServerCertificateValidationCallback = (s, c, h, e) => true; } - else if (_x509ChainOptions.TryGetCustomRemoteCertificateValidationCallback(out var callback)) - { - client.ServerCertificateValidationCallback = (sender, cert, chain, errors) => - { - return callback(new X509Certificate2(cert), chain, errors); - }; - } if (!_globalSettings.Mail.Smtp.StartTls && !_globalSettings.Mail.Smtp.Ssl && _globalSettings.Mail.Smtp.Port == 25) diff --git a/test/Core.IntegrationTest/MailKitSmtpMailDeliveryServiceTests.cs b/test/Core.IntegrationTest/MailKitSmtpMailDeliveryServiceTests.cs index 38c18f26f9..06f333b05c 100644 --- a/test/Core.IntegrationTest/MailKitSmtpMailDeliveryServiceTests.cs +++ b/test/Core.IntegrationTest/MailKitSmtpMailDeliveryServiceTests.cs @@ -1,13 +1,11 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Bit.Core.Models.Mail; -using Bit.Core.Platform.X509ChainCustomization; using Bit.Core.Services; using Bit.Core.Settings; using MailKit.Security; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using Rnwood.SmtpServer; using Rnwood.SmtpServer.Extensions.Auth; using Xunit.Abstractions; @@ -104,8 +102,7 @@ public class MailKitSmtpMailDeliveryServiceTests var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( globalSettings, - NullLogger.Instance, - Options.Create(new X509ChainOptions()) + NullLogger.Instance ); await Assert.ThrowsAsync( @@ -118,117 +115,6 @@ public class MailKitSmtpMailDeliveryServiceTests ); } - [Fact] - public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_Works() - { - // If an SMTP server is using a self signed cert we will in the future - // allow a custom location for certificates to be stored and the certitifactes - // stored there will also be trusted. - var port = RandomPort(); - var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert); - using var smtpServer = new SmtpServer(behavior); - smtpServer.Start(); - - var globalSettings = GetSettings(gs => - { - gs.Mail.Smtp.Port = port; - gs.Mail.Smtp.Ssl = true; - }); - - var x509ChainOptions = new X509ChainOptions - { - AdditionalCustomTrustCertificates = - [ - _selfSignedCert, - ], - }; - - var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( - globalSettings, - NullLogger.Instance, - Options.Create(x509ChainOptions) - ); - - var tcs = new TaskCompletionSource(); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - cts.Token.Register(() => _ = tcs.TrySetCanceled()); - - behavior.MessageReceivedEventHandler += (sender, args) => - { - if (args.Message.Recipients.Contains("test1@example.com")) - { - tcs.SetResult(); - } - return Task.CompletedTask; - }; - - await mailKitDeliveryService.SendEmailAsync(new MailMessage - { - Subject = "Test", - ToEmails = ["test1@example.com"], - TextContent = "Hi", - }, cts.Token); - - // Wait for email - await tcs.Task; - } - - [Fact] - public async Task SendEmailAsync_SmtpServerUsingSelfSignedCert_CertInCustomLocation_WithUnrelatedCerts_Works() - { - // If an SMTP server is using a self signed cert we will in the future - // allow a custom location for certificates to be stored and the certitifactes - // stored there will also be trusted. - var port = RandomPort(); - var behavior = new DefaultServerBehaviour(false, port, _selfSignedCert); - using var smtpServer = new SmtpServer(behavior); - smtpServer.Start(); - - var globalSettings = GetSettings(gs => - { - gs.Mail.Smtp.Port = port; - gs.Mail.Smtp.Ssl = true; - }); - - var x509ChainOptions = new X509ChainOptions - { - AdditionalCustomTrustCertificates = - [ - _selfSignedCert, - CreateSelfSignedCert("example.com"), - ], - }; - - var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( - globalSettings, - NullLogger.Instance, - Options.Create(x509ChainOptions) - ); - - var tcs = new TaskCompletionSource(); - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - cts.Token.Register(() => _ = tcs.TrySetCanceled()); - - behavior.MessageReceivedEventHandler += (sender, args) => - { - if (args.Message.Recipients.Contains("test1@example.com")) - { - tcs.SetResult(); - } - return Task.CompletedTask; - }; - - await mailKitDeliveryService.SendEmailAsync(new MailMessage - { - Subject = "Test", - ToEmails = ["test1@example.com"], - TextContent = "Hi", - }, cts.Token); - - // Wait for email - await tcs.Task; - } - [Fact] public async Task SendEmailAsync_Succeeds_WhenCertIsSelfSigned_ServerIsTrusted() { @@ -249,8 +135,7 @@ public class MailKitSmtpMailDeliveryServiceTests var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( globalSettings, - NullLogger.Instance, - Options.Create(new X509ChainOptions()) + NullLogger.Instance ); var tcs = new TaskCompletionSource(); @@ -296,8 +181,7 @@ public class MailKitSmtpMailDeliveryServiceTests var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( globalSettings, - NullLogger.Instance, - Options.Create(new X509ChainOptions()) + NullLogger.Instance ); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); @@ -332,8 +216,7 @@ public class MailKitSmtpMailDeliveryServiceTests var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( globalSettings, - NullLogger.Instance, - Options.Create(new X509ChainOptions()) + NullLogger.Instance ); var tcs = new TaskCompletionSource(); @@ -399,8 +282,7 @@ public class MailKitSmtpMailDeliveryServiceTests var mailKitDeliveryService = new MailKitSmtpMailDeliveryService( globalSettings, - NullLogger.Instance, - Options.Create(new X509ChainOptions()) + NullLogger.Instance ); var tcs = new TaskCompletionSource(); diff --git a/test/Core.Test/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensionsTests.cs b/test/Core.Test/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensionsTests.cs deleted file mode 100644 index cec8a2a39d..0000000000 --- a/test/Core.Test/Platform/X509ChainCustomization/X509ChainCustomizationServiceCollectionExtensionsTests.cs +++ /dev/null @@ -1,359 +0,0 @@ -using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using Bit.Core.Platform.X509ChainCustomization; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Platform.X509ChainCustomization; - -public class X509ChainCustomizationServiceCollectionExtensionsTests -{ - private static X509Certificate2 CreateSelfSignedCert(string commonName) - { - using var rsa = RSA.Create(2048); - var certRequest = new CertificateRequest($"CN={commonName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - return certRequest.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddYears(1)); - } - - [Fact] - public async Task OptionsPatternReturnsCachedValue() - { - var tempDir = Directory.CreateTempSubdirectory("certs"); - - var tempCert = Path.Combine(tempDir.FullName, "test.crt"); - await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); - - var services = CreateServices((gs, environment, config) => - { - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; - }); - - // Create options once - var firstOptions = services.GetRequiredService>().Value; - - Assert.NotNull(firstOptions.AdditionalCustomTrustCertificates); - var cert = Assert.Single(firstOptions.AdditionalCustomTrustCertificates); - Assert.Equal("CN=localhost", cert.Subject); - - // Since the second resolution should have cached values, deleting the file during operation - // should have no impact. - File.Delete(tempCert); - - // This is expected to be a cached version and doesn't actually need to go and read the file system - var secondOptions = services.GetRequiredService>().Value; - Assert.Same(firstOptions, secondOptions); - - // This is the same reference as the first one so it shouldn't be different but just in case. - Assert.NotNull(secondOptions.AdditionalCustomTrustCertificates); - Assert.Single(secondOptions.AdditionalCustomTrustCertificates); - } - - [Fact] - public async Task DoesNotProvideCustomCallbackOnCloud() - { - var tempDir = Directory.CreateTempSubdirectory("certs"); - - var tempCert = Path.Combine(tempDir.FullName, "test.crt"); - await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); - - var options = CreateOptions((gs, environment, config) => - { - gs.SelfHosted = false; - - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; - }); - - Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); - } - - [Fact] - public async Task ManuallyAddingOptionsTakesPrecedence() - { - var tempDir = Directory.CreateTempSubdirectory("certs"); - - var tempCert = Path.Combine(tempDir.FullName, "test.crt"); - await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); - - var services = CreateServices((gs, environment, config) => - { - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; - }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")]; - }); - }); - - var options = services.GetRequiredService>().Value; - - Assert.True(options.TryGetCustomRemoteCertificateValidationCallback(out var callback)); - var cert = Assert.Single(options.AdditionalCustomTrustCertificates); - Assert.Equal("CN=example.com", cert.Subject); - - var fakeLogCollector = services.GetFakeLogCollector(); - - Assert.Contains(fakeLogCollector.GetSnapshot(), - r => r.Message == $"Additional custom trust certificates were added directly, skipping loading them from '{tempDir}'"); - } - - [Fact] - public void NullCustomDirectory_SkipsTryingToLoad() - { - var services = CreateServices((gs, environment, config) => - { - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = null; - }); - - var options = services.GetRequiredService>().Value; - - Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); - } - - [Theory] - [InlineData("Development", LogLevel.Debug)] - [InlineData("Production", LogLevel.Warning)] - public void CustomDirectoryDoesNotExist_Logs(string environment, LogLevel logLevel) - { - var fakeDir = "/fake/dir/that/does/not/exist"; - var services = CreateServices((gs, hostEnvironment, config) => - { - hostEnvironment.EnvironmentName = environment; - - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = fakeDir; - }); - - var options = services.GetRequiredService>().Value; - - Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); - - var fakeLogCollector = services.GetFakeLogCollector(); - - Assert.Contains(fakeLogCollector.GetSnapshot(), - r => r.Message == $"An additional custom trust certificate directory was given '{fakeDir}' but that directory does not exist." - && r.Level == logLevel - ); - } - - [Fact] - public async Task NamedOptions_NotConfiguredAsync() - { - // To help make sure this fails for the right reason we should add certs to the directory - var tempDir = Directory.CreateTempSubdirectory("certs"); - - var tempCert = Path.Combine(tempDir.FullName, "test.crt"); - await File.WriteAllBytesAsync(tempCert, CreateSelfSignedCert("localhost").Export(X509ContentType.Cert)); - - var services = CreateServices((gs, environment, config) => - { - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; - }); - - var options = services.GetRequiredService>(); - - var namedOptions = options.Get("SomeName"); - - Assert.Null(namedOptions.AdditionalCustomTrustCertificates); - } - - [Fact] - public void CustomLocation_NoCertificates_Logs() - { - var tempDir = Directory.CreateTempSubdirectory("certs"); - var services = CreateServices((gs, hostEnvironment, config) => - { - config["X509ChainOptions:AdditionalCustomTrustCertificatesDirectory"] = tempDir.FullName; - }); - - var options = services.GetRequiredService>().Value; - - Assert.False(options.TryGetCustomRemoteCertificateValidationCallback(out _)); - - var fakeLogCollector = services.GetFakeLogCollector(); - - Assert.Contains(fakeLogCollector.GetSnapshot(), - r => r.Message == $"No additional custom trust certificates were found in '{tempDir.FullName}'" - ); - } - - [Fact] - public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_Works() - { - var selfSignedCertificate = CreateSelfSignedCert("localhost"); - await using var app = await CreateServerAsync(55555, options => - { - options.ServerCertificate = selfSignedCertificate; - }); - - var services = CreateServices((gs, environment, config) => { }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = [selfSignedCertificate]; - }); - }); - - var httpClient = services.GetRequiredService().CreateClient(); - - var response = await httpClient.GetStringAsync("https://localhost:55555"); - Assert.Equal("Hi", response); - } - - [Fact] - public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateNotConfigured_Throws() - { - var selfSignedCertificate = CreateSelfSignedCert("localhost"); - await using var app = await CreateServerAsync(55556, options => - { - options.ServerCertificate = selfSignedCertificate; - }); - - var services = CreateServices((gs, environment, config) => { }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = [CreateSelfSignedCert("example.com")]; - }); - }); - - var httpClient = services.GetRequiredService().CreateClient(); - - var requestException = await Assert.ThrowsAsync(async () => await httpClient.GetStringAsync("https://localhost:55556")); - Assert.NotNull(requestException.InnerException); - var authenticationException = Assert.IsAssignableFrom(requestException.InnerException); - Assert.Equal("The remote certificate was rejected by the provided RemoteCertificateValidationCallback.", authenticationException.Message); - } - - [Fact] - public async Task CallHttpWithSelfSignedCert_SelfSignedCertificateConfigured_WithExtraCert_Works() - { - var selfSignedCertificate = CreateSelfSignedCert("localhost"); - await using var app = await CreateServerAsync(55557, options => - { - options.ServerCertificate = selfSignedCertificate; - }); - - var services = CreateServices((gs, environment, config) => { }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = [selfSignedCertificate, CreateSelfSignedCert("example.com")]; - }); - }); - - var httpClient = services.GetRequiredService().CreateClient(); - - var response = await httpClient.GetStringAsync("https://localhost:55557"); - Assert.Equal("Hi", response); - } - - [Fact] - public async Task CallHttp_ReachingOutToServerTrustedThroughSystemCA() - { - var services = CreateServices((gs, environment, config) => { }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = []; - }); - }); - - var httpClient = services.GetRequiredService().CreateClient(); - - var response = await httpClient.GetAsync("https://example.com"); - response.EnsureSuccessStatusCode(); - } - - [Fact] - public async Task CallHttpWithCustomTrustForSelfSigned_ReachingOutToServerTrustedThroughSystemCA() - { - var selfSignedCertificate = CreateSelfSignedCert("localhost"); - var services = CreateServices((gs, environment, config) => { }, services => - { - services.Configure(options => - { - options.AdditionalCustomTrustCertificates = [selfSignedCertificate]; - }); - }); - - var httpClient = services.GetRequiredService().CreateClient(); - - var response = await httpClient.GetAsync("https://example.com"); - response.EnsureSuccessStatusCode(); - } - - private static async Task CreateServerAsync(int port, Action configure) - { - var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions()); - builder.Services.AddRoutingCore(); - builder.WebHost.UseKestrelCore() - .ConfigureKestrel(options => - { - options.ListenLocalhost(port, listenOptions => - { - listenOptions.UseHttps(httpsOptions => - { - configure(httpsOptions); - }); - }); - }); - - var app = builder.Build(); - - app.MapGet("/", () => "Hi"); - - await app.StartAsync(); - - return app; - } - - private static X509ChainOptions CreateOptions(Action> configure, Action? after = null) - { - var services = CreateServices(configure, after); - return services.GetRequiredService>().Value; - } - - private static IServiceProvider CreateServices(Action> configure, Action? after = null) - { - var globalSettings = new GlobalSettings - { - // A solid default for these tests as these settings aren't allowed to work in cloud. - SelfHosted = true, - }; - var hostEnvironment = Substitute.For(); - hostEnvironment.EnvironmentName = "Development"; - var config = new Dictionary(); - - configure(globalSettings, hostEnvironment, config); - - var services = new ServiceCollection(); - services.AddLogging(logging => - { - logging.SetMinimumLevel(LogLevel.Debug); - logging.AddFakeLogging(); - }); - services.AddSingleton(globalSettings); - services.AddSingleton(hostEnvironment); - services.AddSingleton( - new ConfigurationBuilder() - .AddInMemoryCollection(config) - .Build() - ); - - services.AddX509ChainCustomization(); - - after?.Invoke(services); - - return services.BuildServiceProvider(); - } -} diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 35c2f8fe3b..89d9a211e0 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -4,11 +4,9 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business; using Bit.Core.Entities; -using Bit.Core.Platform.X509ChainCustomization; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using NSubstitute; using Xunit; @@ -138,7 +136,7 @@ public class HandlebarsMailServiceTests SiteName = "Bitwarden", }; - var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For>(), Options.Create(new X509ChainOptions())); + var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For>()); var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService()); diff --git a/test/Core.Test/Services/MailKitSmtpMailDeliveryServiceTests.cs b/test/Core.Test/Services/MailKitSmtpMailDeliveryServiceTests.cs index 06ee99dbef..4e7e36fe02 100644 --- a/test/Core.Test/Services/MailKitSmtpMailDeliveryServiceTests.cs +++ b/test/Core.Test/Services/MailKitSmtpMailDeliveryServiceTests.cs @@ -1,8 +1,6 @@ -using Bit.Core.Platform.X509ChainCustomization; -using Bit.Core.Services; +using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using NSubstitute; using Xunit; @@ -25,8 +23,7 @@ public class MailKitSmtpMailDeliveryServiceTests _sut = new MailKitSmtpMailDeliveryService( _globalSettings, - _logger, - Options.Create(new X509ChainOptions()) + _logger ); } From 765c02b7d20ad3c5bb73390fa1250d5294144ab1 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:16:16 -0400 Subject: [PATCH 070/326] [BRE-1018] improve database test error messaging (#6103) * improve database test error messaging * removing repetitive logic --- .github/workflows/test-database.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 23722e2e8d..65417f7529 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -229,11 +229,27 @@ jobs: - name: Validate XML run: | if grep -q "" "report.xml"; then - echo - echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project." + echo "ERROR: Migration files are not in sync with the SQL project" + echo "" + echo "Check these locations:" + echo " - Migration scripts: util/Migrator/DbScripts/" + echo " - SQL project files: src/Sql/" + echo " - Download 'report.xml' artifact for full details" + echo "" + + # Show actual SQL differences - exclude database setup commands + if [ -s "diff.sql" ]; then + echo "Key SQL differences:" + # Show meaningful schema differences, filtering out database setup noise + grep -E "^(CREATE|DROP|ALTER)" diff.sql | grep -v "ALTER DATABASE" | grep -v "DatabaseName" | head -5 + echo "" + fi + + echo "Common causes: naming differences (underscores, case), missing objects, or definition mismatches" + exit 1 else - echo "Report looks good" + echo "SUCCESS: Database validation passed" fi shell: bash From 4963911d7e2c96f4e80c571ac6adf4e527bf5cfe Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 21 Jul 2025 13:44:40 -0500 Subject: [PATCH 071/326] [PM-23756] Report summary endpoints- mocked (#6092) --- src/Api/Dirt/Controllers/ReportsController.cs | 123 ++++++++++++ .../OrganizationReportSummaryModel.cs | 9 + test/Api.Test/Dirt/ReportsControllerTests.cs | 183 ++++++++++++++++++ 3 files changed, 315 insertions(+) create mode 100644 src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index 8bb8b5e487..e7c7e4a9bf 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -281,4 +281,127 @@ public class ReportsController : Controller } return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId); } + + /// + /// Gets the Organization Report Summary for an organization. + /// This includes the latest report's encrypted data, encryption key, and date. + /// This is a mock implementation and should be replaced with actual data retrieval logic. + /// + /// + /// Min date (example: 2023-01-01) + /// Max date (example: 2023-12-31) + /// + /// + [HttpGet("organization-report-summary/{orgId}")] + public IEnumerable GetOrganizationReportSummary( + [FromRoute] Guid orgId, + [FromQuery] DateOnly from, + [FromQuery] DateOnly to) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(orgId); + + // FIXME: remove this mock class when actual data retrieval is implemented + return MockOrganizationReportSummary.GetMockData() + .Where(_ => _.OrganizationId == orgId + && _.Date >= from.ToDateTime(TimeOnly.MinValue) + && _.Date <= to.ToDateTime(TimeOnly.MaxValue)); + } + + /// + /// Creates a new Organization Report Summary for an organization. + /// This is a mock implementation and should be replaced with actual creation logic. + /// + /// + /// Returns 204 Created with the created OrganizationReportSummaryModel + /// + [HttpPost("organization-report-summary")] + public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(model.OrganizationId); + + // TODO: Implement actual creation logic + + // Returns 204 No Content as a placeholder + return NoContent(); + } + + [HttpPut("organization-report-summary")] + public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) + { + if (!ModelState.IsValid) + { + throw new BadRequestException(ModelState); + } + + GuardOrganizationAccess(model.OrganizationId); + + // TODO: Implement actual update logic + + // Returns 204 No Content as a placeholder + return NoContent(); + } + + private void GuardOrganizationAccess(Guid organizationId) + { + if (!_currentContext.AccessReports(organizationId).Result) + { + throw new NotFoundException(); + } + } + + // FIXME: remove this mock class when actual data retrieval is implemented + private class MockOrganizationReportSummary + { + public static List GetMockData() + { + return new List + { + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=", + EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=", + Date = DateTime.UtcNow + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=", + EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=", + EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=", + EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + new OrganizationReportSummaryModel + { + OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), + EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=", + EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=", + Date = DateTime.UtcNow.AddMonths(-1) + }, + }; + } + } } diff --git a/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs new file mode 100644 index 0000000000..d912fb699e --- /dev/null +++ b/src/Api/Dirt/Models/Response/OrganizationReportSummaryModel.cs @@ -0,0 +1,9 @@ +namespace Bit.Api.Dirt.Models.Response; + +public class OrganizationReportSummaryModel +{ + public Guid OrganizationId { get; set; } + public required string EncryptedData { get; set; } + public required string EncryptionKey { get; set; } + public DateTime Date { get; set; } +} diff --git a/test/Api.Test/Dirt/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs index af285d8b85..4636406df5 100644 --- a/test/Api.Test/Dirt/ReportsControllerTests.cs +++ b/test/Api.Test/Dirt/ReportsControllerTests.cs @@ -1,12 +1,14 @@ using AutoFixture; using Bit.Api.Dirt.Controllers; using Bit.Api.Dirt.Models; +using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; @@ -280,4 +282,185 @@ public class ReportsControllerTests _ = sutProvider.GetDependency() .Received(0); } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ReturnsNoContent_WhenAccessGranted(SutProvider sutProvider) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key", + Date = DateTime.UtcNow + }; + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.CreateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(SutProvider sutProvider) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key", + Date = DateTime.UtcNow + }; + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws( + () => sutProvider.Sut.CreateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void GetOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws( + () => sutProvider.Sut.GetOrganizationReportSummary(orgId, DateOnly.FromDateTime(DateTime.UtcNow), DateOnly.FromDateTime(DateTime.UtcNow))); + } + + [Theory, BitAutoData] + public void GetOrganizationReportSummary_returnsExpectedResult( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var dates = new[] + { + DateOnly.FromDateTime(DateTime.UtcNow), + DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-1)) + }; + + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.GetOrganizationReportSummary(orgId, dates[0], dates[1]); + + // Assert + Assert.NotNull(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.CreateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void CreateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.AddModelError("key", "error"); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.CreateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(true); + + // Act + var result = sutProvider.Sut.UpdateOrganizationReportSummary(model); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.AddModelError("key", "error"); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); + } + + [Theory, BitAutoData] + public void UpdateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( + SutProvider sutProvider + ) + { + // Arrange + var orgId = Guid.NewGuid(); + var model = new OrganizationReportSummaryModel + { + OrganizationId = orgId, + EncryptedData = "mock-data", + EncryptionKey = "mock-key" + }; + sutProvider.Sut.ModelState.Clear(); + sutProvider.GetDependency().AccessReports(orgId).Returns(false); + + // Act & Assert + Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); + } } From 302457618170f165a51354cab1128fe2c89f743d Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:04:16 -0400 Subject: [PATCH 072/326] Wildcard for dirt subdirectories (#6096) --- .github/CODEOWNERS | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 51c5f8c8e1..07f9917a5e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -50,11 +50,7 @@ src/Core/IdentityServer @bitwarden/team-auth-dev **/Tools @bitwarden/team-tools-dev # Dirt (Data Insights & Reporting) team -src/Api/Controllers/Dirt @bitwarden/team-data-insights-and-reporting-dev -src/Core/Dirt @bitwarden/team-data-insights-and-reporting-dev -src/Infrastructure.Dapper/Dirt @bitwarden/team-data-insights-and-reporting-dev -test/Api.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev -test/Core.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev +**/Dirt @bitwarden/team-data-insights-and-reporting-dev # Vault team **/Vault @bitwarden/team-vault-dev From bdadf2af0163a59a739bc2651de379194e829e78 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 21 Jul 2025 16:43:30 -0400 Subject: [PATCH 073/326] Document database projects and complete EDD support (#5855) * Document database projects and complete EDD support * Remove an old remnant of a now-unused 'future' state * Sync finalization scripts * Fix conflict * Fix some script issues --- .github/CODEOWNERS | 2 +- ...b_scripts.yml => _move_edd_db_scripts.yml} | 86 ++++++++++++------- .github/workflows/repository-management.yml | 6 +- src/Sql/Sql.sqlproj | 2 - src/Sql/dbo_finalization/.gitkeep | 0 util/Migrator/Migrator.csproj | 1 + util/Migrator/MigratorConstants.cs | 2 +- util/Migrator/README.md | 7 ++ util/MsSqlMigratorUtility/README.md | 16 ++++ util/MySqlMigrations/README.md | 5 ++ util/PostgresMigrations/README.md | 5 ++ util/SqliteMigrations/README.md | 5 ++ 12 files changed, 101 insertions(+), 36 deletions(-) rename .github/workflows/{_move_finalization_db_scripts.yml => _move_edd_db_scripts.yml} (66%) create mode 100644 src/Sql/dbo_finalization/.gitkeep create mode 100644 util/Migrator/README.md create mode 100644 util/MsSqlMigratorUtility/README.md create mode 100644 util/MySqlMigrations/README.md create mode 100644 util/PostgresMigrations/README.md create mode 100644 util/SqliteMigrations/README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 07f9917a5e..88cfc71256 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,7 +14,7 @@ .github/workflows/publish.yml @bitwarden/dept-bre ## These are shared workflows ## -.github/workflows/_move_finalization_db_scripts.yml +.github/workflows/_move_edd_db_scripts.yml .github/workflows/release.yml # Database Operations for database changes diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_edd_db_scripts.yml similarity index 66% rename from .github/workflows/_move_finalization_db_scripts.yml rename to .github/workflows/_move_edd_db_scripts.yml index 33d828fef7..98fe4f1f05 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_edd_db_scripts.yml @@ -1,5 +1,5 @@ -name: _move_finalization_db_scripts -run-name: Move finalization database scripts +name: _move_edd_db_scripts +run-name: Move EDD database scripts on: workflow_call: @@ -17,7 +17,8 @@ jobs: id-token: write outputs: migration_filename_prefix: ${{ steps.prefix.outputs.prefix }} - copy_finalization_scripts: ${{ steps.check-finalization-scripts-existence.outputs.copy_finalization_scripts }} + copy_edd_scripts: ${{ steps.check-script-existence.outputs.copy_edd_scripts }} + steps: - name: Log in to Azure uses: bitwarden/gh-actions/azure-login@main @@ -45,17 +46,17 @@ jobs: id: prefix run: echo "prefix=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - - name: Check if any files in DB finalization directory - id: check-finalization-scripts-existence + - name: Check if any files in DB transition or finalization directories + id: check-script-existence run: | - if [ -f util/Migrator/DbScripts_finalization/* ]; then - echo "copy_finalization_scripts=true" >> $GITHUB_OUTPUT + if [ -f util/Migrator/DbScripts_transition/* -o -f util/Migrator/DbScripts_finalization/* ]; then + echo "copy_edd_scripts=true" >> $GITHUB_OUTPUT else - echo "copy_finalization_scripts=false" >> $GITHUB_OUTPUT + echo "copy_edd_scripts=false" >> $GITHUB_OUTPUT fi - move-finalization-db-scripts: - name: Move finalization database scripts + move-scripts: + name: Move scripts runs-on: ubuntu-22.04 needs: setup permissions: @@ -63,9 +64,9 @@ jobs: pull-requests: write id-token: write actions: read - if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} + if: ${{ needs.setup.outputs.copy_edd_scripts == 'true' }} steps: - - name: Checkout + - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 @@ -74,35 +75,62 @@ jobs: id: branch_name env: PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }} - run: echo "branch_name=move_finalization_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 - - name: Move DbScripts_finalization + - name: Move scripts and finalization database schema id: move-files env: PREFIX: ${{ needs.setup.outputs.migration_filename_prefix }} run: | - src_dir="util/Migrator/DbScripts_finalization" + # scripts + moved_files="Migration scripts moved:\n\n" + + src_dirs="util/Migrator/DbScripts_transition,util/Migrator/DbScripts_finalization" dest_dir="util/Migrator/DbScripts" i=0 - moved_files="" - for file in "$src_dir"/*; do - filenumber=$(printf "%02d" $i) + for src_dir in ${src_dirs//,/ }; do + for file in "$src_dir"/*; do + filenumber=$(printf "%02d" $i) - filename=$(basename "$file") - new_filename="${PREFIX}_${filenumber}_${filename}" - dest_file="$dest_dir/$new_filename" + filename=$(basename "$file") + new_filename="${PREFIX}_${filenumber}_${filename}" + dest_file="$dest_dir/$new_filename" - mv "$file" "$dest_file" - moved_files="$moved_files \n $filename -> $new_filename" + # Replace any finalization references due to the move + sed -i -e 's/dbo_finalization/dbo/g' "$file" - i=$((i+1)) + mv "$file" "$dest_file" + moved_files="$moved_files \n $filename -> $new_filename" + + i=$((i+1)) + done done + + # schema + moved_files="$moved_files\n\nFinalization scripts moved:\n\n" + + src_dir="src/Sql/dbo_finalization" + dest_dir="src/Sql/dbo" + + # sync finalization schema back to dbo, maintaining structure + rsync -r "$src_dir/" "$dest_dir/" + rm -rf $src_dir/* + + # Replace any finalization references due to the move + find ./src/Sql/dbo -name "*.sql" -type f -exec sed -i \ + -e 's/\[dbo_finalization\]/[dbo]/g' \ + -e 's/dbo_finalization\./dbo./g' {} + + + for file in "$src_dir"/**/*; do + moved_files="$moved_files \n $file" + done + echo "moved_files=$moved_files" >> $GITHUB_OUTPUT - name: Log in to Azure @@ -139,7 +167,7 @@ jobs: git config --local user.name "bitwarden-devops-bot" if [ -n "$(git status --porcelain)" ]; then git add . - git commit -m "Move DbScripts_finalization to DbScripts" -a + git commit -m "Move EDD database scripts" -a git push -u origin ${{ steps.branch_name.outputs.branch_name }} echo "pr_needed=true" >> $GITHUB_OUTPUT else @@ -155,16 +183,16 @@ jobs: BRANCH: ${{ steps.branch_name.outputs.branch_name }} GH_TOKEN: ${{ github.token }} MOVED_FILES: ${{ steps.move-files.outputs.moved_files }} - TITLE: "Move finalization database scripts" + TITLE: "Move EDD database scripts" run: | PR_URL=$(gh pr create --title "$TITLE" \ --base "main" \ --head "$BRANCH" \ --label "automated pr" \ --body " - ## Automated movement of DbScripts_finalization to DbScripts + Automated movement of EDD database scripts. - ## Files moved: + Files moved: $(echo -e "$MOVED_FILES") ") echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT @@ -175,5 +203,5 @@ jobs: env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} with: - message: "Created PR for moving DbScripts_finalization to DbScripts: ${{ steps.create-pr.outputs.pr_url }}" + message: "Created PR for moving EDD database scripts: ${{ steps.create-pr.outputs.pr_url }}" status: ${{ job.status }} diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 2d75cbcb4a..b5d6db69d4 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -228,8 +228,8 @@ jobs: git switch --quiet --create $BRANCH_NAME git push --quiet --set-upstream origin $BRANCH_NAME - move_future_db_scripts: - name: Move finalization database scripts + move_edd_db_scripts: + name: Move EDD database scripts needs: cut_branch - uses: ./.github/workflows/_move_finalization_db_scripts.yml + uses: ./.github/workflows/_move_edd_db_scripts.yml secrets: inherit diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 849fd3bdfd..1a7530321e 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -11,8 +11,6 @@ - - diff --git a/src/Sql/dbo_finalization/.gitkeep b/src/Sql/dbo_finalization/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/util/Migrator/Migrator.csproj b/util/Migrator/Migrator.csproj index b425babea3..bef6dadfb8 100644 --- a/util/Migrator/Migrator.csproj +++ b/util/Migrator/Migrator.csproj @@ -3,6 +3,7 @@ + diff --git a/util/Migrator/MigratorConstants.cs b/util/Migrator/MigratorConstants.cs index 1ffcfcfe22..bf1bccb6c2 100644 --- a/util/Migrator/MigratorConstants.cs +++ b/util/Migrator/MigratorConstants.cs @@ -4,5 +4,5 @@ public static class MigratorConstants { public const string SqlTableJournalName = "Migration"; public const string DefaultMigrationsFolderName = "DbScripts"; - public const string TransitionMigrationsFolderName = "DbScripts_data_migration"; + public const string TransitionMigrationsFolderName = "DbScripts_transition"; } diff --git a/util/Migrator/README.md b/util/Migrator/README.md new file mode 100644 index 0000000000..950c2b682e --- /dev/null +++ b/util/Migrator/README.md @@ -0,0 +1,7 @@ +# Bitwarden Database Migrator + +A class library leveraged by [utilities](../MsSqlMigratorUtility) and [hosted applications](/src/Admin/HostedServices/DatabaseMigrationHostedService.cs) to perform SQL database migrations. A [MSSQL migrator](./SqlServerDbMigrator.cs) exists here as the default use case. + +In production environments the Migrator is typically executed during application startup or as part of CI/CD pipelines to ensure database schemas are up-to-date before application deployment. + +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. diff --git a/util/MsSqlMigratorUtility/README.md b/util/MsSqlMigratorUtility/README.md new file mode 100644 index 0000000000..469d1bb43e --- /dev/null +++ b/util/MsSqlMigratorUtility/README.md @@ -0,0 +1,16 @@ +# Bitwarden MSSQL Database Migrator Utility + +A command-line utility for performing MSSQL database migrations for Bitwarden's self-hosted and cloud deployments. + +## Overview + +The MSSQL Migrator Utility is a specialized tool that leverages the [Migrator library](../Migrator) to handle MSSQL database migrations. The utility uses [DbUp](https://dbup.github.io/) to handle the execution and tracking of database migrations. It runs SQL scripts in order, tracking which scripts have been executed to avoid duplicate runs. + +## Features + +- Command-line interface for executing database migrations +- Integration with DbUp for reliable migration management +- Execution inside or outside of transactions for different application scenarios +- Script execution tracking to prevent duplicate migrations and support retries + +See the [documentation](https://contributing.bitwarden.com/getting-started/server/database/mssql/#updating-the-database) for usage. diff --git a/util/MySqlMigrations/README.md b/util/MySqlMigrations/README.md new file mode 100644 index 0000000000..3373aabaee --- /dev/null +++ b/util/MySqlMigrations/README.md @@ -0,0 +1,5 @@ +# Bitwarden MySQL Database Migrator + +A class library leveraged by [hosted applications](/src/Admin/HostedServices/DatabaseMigrationHostedService.cs) to perform MySQL database migrations via Entity Framework. + +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. diff --git a/util/PostgresMigrations/README.md b/util/PostgresMigrations/README.md new file mode 100644 index 0000000000..4436fbe72f --- /dev/null +++ b/util/PostgresMigrations/README.md @@ -0,0 +1,5 @@ +# Bitwarden PostgreSQL Database Migrator + +A class library leveraged by [hosted applications](/src/Admin/HostedServices/DatabaseMigrationHostedService.cs) to perform PostgreSQL database migrations via Entity Framework. + +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. diff --git a/util/SqliteMigrations/README.md b/util/SqliteMigrations/README.md new file mode 100644 index 0000000000..ff55a7c1b7 --- /dev/null +++ b/util/SqliteMigrations/README.md @@ -0,0 +1,5 @@ +# Bitwarden SQLite Database Migrator + +A class library leveraged by [hosted applications](/src/Admin/HostedServices/DatabaseMigrationHostedService.cs) to perform SQLite database migrations via Entity Framework. + +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. From f4e1e2f1f7c613b4bce4c002a811a7cf78da223d Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:02:13 -0400 Subject: [PATCH 074/326] [PM-17562] Add support for null/all event type (#6100) * [PM-17562] Add support for null/all event type * Address PR Feedback * Adjusted SQL scripts per feedback --- ...ionIntegrationConfigurationRequestModel.cs | 7 +- ...onIntegrationConfigurationResponseModel.cs | 2 +- .../OrganizationIntegrationConfiguration.cs | 2 +- ...nizationIntegrationConfigurationDetails.cs | 2 +- ...grationConfigurationDetailsCacheService.cs | 24 +- .../OrganizationIntegrationConfiguration.sql | 2 +- ...onConfigurationDetailsCacheServiceTests.cs | 52 +- ...peOrganizationIntegrationConfiguration.sql | 102 + ...zationIntegrationConfiguration.Designer.cs | 3263 ++++++++++++++++ ...ypeOrganizationIntegrationConfiguration.cs | 35 + .../DatabaseContextModelSnapshot.cs | 2 +- ...zationIntegrationConfiguration.Designer.cs | 3269 +++++++++++++++++ ...ypeOrganizationIntegrationConfiguration.cs | 35 + .../DatabaseContextModelSnapshot.cs | 2 +- ...zationIntegrationConfiguration.Designer.cs | 3252 ++++++++++++++++ ...ypeOrganizationIntegrationConfiguration.cs | 35 + .../DatabaseContextModelSnapshot.cs | 2 +- 17 files changed, 10064 insertions(+), 24 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-07-17_00_AllowNullEventTypeOrganizationIntegrationConfiguration.sql create mode 100644 util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.cs create mode 100644 util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.cs create mode 100644 util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.cs diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index c6dfb49ef3..17e116b8d1 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable + using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; -#nullable enable namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -12,8 +12,7 @@ public class OrganizationIntegrationConfigurationRequestModel { public string? Configuration { get; set; } - [Required] - public EventType EventType { get; set; } + public EventType? EventType { get; set; } public string? Filters { get; set; } diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs index 590a32ee3d..c7906318e8 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationConfigurationResponseModel.cs @@ -25,6 +25,6 @@ public class OrganizationIntegrationConfigurationResponseModel : ResponseModel public string? Configuration { get; set; } public string? Filters { get; set; } public DateTime CreationDate { get; set; } - public EventType EventType { get; set; } + public EventType? EventType { get; set; } public string? Template { get; set; } } diff --git a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs b/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs index 5c2250824e..52934cf7f3 100644 --- a/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Entities/OrganizationIntegrationConfiguration.cs @@ -10,7 +10,7 @@ public class OrganizationIntegrationConfiguration : ITableObject { public Guid Id { get; set; } public Guid OrganizationIntegrationId { get; set; } - public EventType EventType { get; set; } + public EventType? EventType { get; set; } public string? Configuration { get; set; } public string? Template { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs index a184e9ac8e..5fdc760c90 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs @@ -11,7 +11,7 @@ public class OrganizationIntegrationConfigurationDetails public Guid OrganizationId { get; set; } public Guid OrganizationIntegrationId { get; set; } public IntegrationType IntegrationType { get; set; } - public EventType EventType { get; set; } + public EventType? EventType { get; set; } public string? Configuration { get; set; } public string? Filters { get; set; } public string? IntegrationConfiguration { get; set; } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs index 4e4657f824..a63efac62f 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationConfigurationDetailsCacheService.cs @@ -10,7 +10,7 @@ namespace Bit.Core.Services; public class IntegrationConfigurationDetailsCacheService : BackgroundService, IIntegrationConfigurationDetailsCache { - private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType EventType); + private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType? EventType); private readonly IOrganizationIntegrationConfigurationRepository _repository; private readonly ILogger _logger; private readonly TimeSpan _refreshInterval; @@ -19,8 +19,7 @@ public class IntegrationConfigurationDetailsCacheService : BackgroundService, II public IntegrationConfigurationDetailsCacheService( IOrganizationIntegrationConfigurationRepository repository, GlobalSettings globalSettings, - ILogger logger - ) + ILogger logger) { _repository = repository; _logger = logger; @@ -32,10 +31,21 @@ public class IntegrationConfigurationDetailsCacheService : BackgroundService, II IntegrationType integrationType, EventType eventType) { - var key = new IntegrationCacheKey(organizationId, integrationType, eventType); - return _cache.TryGetValue(key, out var value) - ? value - : new List(); + var specificKey = new IntegrationCacheKey(organizationId, integrationType, eventType); + var allEventsKey = new IntegrationCacheKey(organizationId, integrationType, null); + + var results = new List(); + + if (_cache.TryGetValue(specificKey, out var specificConfigs)) + { + results.AddRange(specificConfigs); + } + if (_cache.TryGetValue(allEventsKey, out var fallbackConfigs)) + { + results.AddRange(fallbackConfigs); + } + + return results; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql index b46ca81141..e15d5576eb 100644 --- a/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql +++ b/src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql @@ -2,7 +2,7 @@ CREATE TABLE [dbo].[OrganizationIntegrationConfiguration] ( [Id] UNIQUEIDENTIFIER NOT NULL, [OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL, - [EventType] SMALLINT NOT NULL, + [EventType] SMALLINT NULL, [Configuration] VARCHAR (MAX) NULL, [Template] VARCHAR (MAX) NULL, [CreationDate] DATETIME2 (7) NOT NULL, diff --git a/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs index d24a5afa27..4e87d13caf 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationConfigurationDetailsCacheServiceTests.cs @@ -1,6 +1,7 @@ #nullable enable using System.Text.Json; +using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; @@ -28,18 +29,55 @@ public class IntegrationConfigurationDetailsCacheServiceTests } [Theory, BitAutoData] - public async Task GetConfigurationDetails_KeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config) + public async Task GetConfigurationDetails_SpecificKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config) { + config.EventType = EventType.Cipher_Created; var sutProvider = GetSutProvider([config]); await sutProvider.Sut.RefreshAsync(); var result = sutProvider.Sut.GetConfigurationDetails( config.OrganizationId, config.IntegrationType, - config.EventType); + EventType.Cipher_Created); Assert.Single(result); Assert.Same(config, result[0]); } + [Theory, BitAutoData] + public async Task GetConfigurationDetails_AllEventsKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config) + { + config.EventType = null; + var sutProvider = GetSutProvider([config]); + await sutProvider.Sut.RefreshAsync(); + var result = sutProvider.Sut.GetConfigurationDetails( + config.OrganizationId, + config.IntegrationType, + EventType.Cipher_Created); + Assert.Single(result); + Assert.Same(config, result[0]); + } + + [Theory, BitAutoData] + public async Task GetConfigurationDetails_BothSpecificAndAllEventsKeyExists_ReturnsExpectedList( + OrganizationIntegrationConfigurationDetails specificConfig, + OrganizationIntegrationConfigurationDetails allKeysConfig + ) + { + specificConfig.EventType = EventType.Cipher_Created; + allKeysConfig.EventType = null; + allKeysConfig.OrganizationId = specificConfig.OrganizationId; + allKeysConfig.IntegrationType = specificConfig.IntegrationType; + + var sutProvider = GetSutProvider([specificConfig, allKeysConfig]); + await sutProvider.Sut.RefreshAsync(); + var result = sutProvider.Sut.GetConfigurationDetails( + specificConfig.OrganizationId, + specificConfig.IntegrationType, + EventType.Cipher_Created); + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Template == specificConfig.Template); + Assert.Contains(result, r => r.Template == allKeysConfig.Template); + } + [Theory, BitAutoData] public async Task GetConfigurationDetails_KeyMissing_ReturnsEmptyList(OrganizationIntegrationConfigurationDetails config) { @@ -48,10 +86,12 @@ public class IntegrationConfigurationDetailsCacheServiceTests var result = sutProvider.Sut.GetConfigurationDetails( Guid.NewGuid(), config.IntegrationType, - config.EventType); + config.EventType ?? EventType.Cipher_Created); Assert.Empty(result); } + + [Theory, BitAutoData] public async Task GetConfigurationDetails_ReturnsCachedValue_EvenIfRepositoryChanges(OrganizationIntegrationConfigurationDetails config) { @@ -67,7 +107,7 @@ public class IntegrationConfigurationDetailsCacheServiceTests var result = sutProvider.Sut.GetConfigurationDetails( config.OrganizationId, config.IntegrationType, - config.EventType); + config.EventType ?? EventType.Cipher_Created); Assert.Single(result); Assert.NotEqual("Changed", result[0].Template); // should not yet pick up change from repository @@ -76,7 +116,7 @@ public class IntegrationConfigurationDetailsCacheServiceTests result = sutProvider.Sut.GetConfigurationDetails( config.OrganizationId, config.IntegrationType, - config.EventType); + config.EventType ?? EventType.Cipher_Created); Assert.Single(result); Assert.Equal("Changed", result[0].Template); // Should have the new value } @@ -94,7 +134,7 @@ public class IntegrationConfigurationDetailsCacheServiceTests var results = sutProvider.Sut.GetConfigurationDetails( config1.OrganizationId, config1.IntegrationType, - config1.EventType); + config1.EventType ?? EventType.Cipher_Created); Assert.Equal(2, results.Count); Assert.Contains(results, r => r.Template == config1.Template); diff --git a/util/Migrator/DbScripts/2025-07-17_00_AllowNullEventTypeOrganizationIntegrationConfiguration.sql b/util/Migrator/DbScripts/2025-07-17_00_AllowNullEventTypeOrganizationIntegrationConfiguration.sql new file mode 100644 index 0000000000..06fcba0c38 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-17_00_AllowNullEventTypeOrganizationIntegrationConfiguration.sql @@ -0,0 +1,102 @@ +IF EXISTS ( + SELECT 1 + FROM sys.columns + WHERE object_id = OBJECT_ID(N'[dbo].[OrganizationIntegrationConfiguration]') + AND name = 'EventType' + AND is_nullable = 0 -- Currently NOT NULL +) +BEGIN + ALTER TABLE [dbo].[OrganizationIntegrationConfiguration] + ALTER COLUMN [EventType] SMALLINT NULL +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationIntegrationId UNIQUEIDENTIFIER, + @EventType SMALLINT, + @Configuration VARCHAR(MAX), + @Template VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Filters VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationIntegrationConfiguration] + ( + [Id], + [OrganizationIntegrationId], + [EventType], + [Configuration], + [Template], + [CreationDate], + [RevisionDate], + [Filters] + ) + VALUES + ( + @Id, + @OrganizationIntegrationId, + @EventType, + @Configuration, + @Template, + @CreationDate, + @RevisionDate, + @Filters + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Update] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationIntegrationId UNIQUEIDENTIFIER, + @EventType SMALLINT, + @Configuration VARCHAR(MAX), + @Template VARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Filters VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + +UPDATE + [dbo].[OrganizationIntegrationConfiguration] +SET + [OrganizationIntegrationId] = @OrganizationIntegrationId, + [EventType] = @EventType, + [Configuration] = @Configuration, + [Template] = @Template, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [Filters] = @Filters +WHERE + [Id] = @Id +END +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationIntegrationConfigurationView] +AS +SELECT + * +FROM + [dbo].[OrganizationIntegrationConfiguration] +GO + +CREATE OR ALTER VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView] +AS +SELECT + oi.[OrganizationId], + oi.[Type] AS [IntegrationType], + oic.[EventType], + oic.[Configuration], + oi.[Configuration] AS [IntegrationConfiguration], + oic.[Template], + oic.[Filters] +FROM + [dbo].[OrganizationIntegrationConfiguration] oic + INNER JOIN + [dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId] +GO diff --git a/util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs b/util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..7a01d4c8db --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3263 @@ +// +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("20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration")] + partial class AllowNullEventTypeOrganizationIntegrationConfiguration + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.cs b/util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..0bcd30bb73 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250716205130_AllowNullEventTypeOrganizationIntegrationConfiguration.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AllowNullEventTypeOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index fbe40a8d2b..89246bcff0 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -313,7 +313,7 @@ namespace Bit.MySqlMigrations.Migrations b.Property("CreationDate") .HasColumnType("datetime(6)"); - b.Property("EventType") + b.Property("EventType") .HasColumnType("int"); b.Property("Filters") diff --git a/util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs b/util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..b621142064 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3269 @@ +// +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("20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration")] + partial class AllowNullEventTypeOrganizationIntegrationConfiguration + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.cs b/util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..8fc3150cc1 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250716205125_AllowNullEventTypeOrganizationIntegrationConfiguration.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AllowNullEventTypeOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "integer", + nullable: true, + oldClrType: typeof(int), + oldType: "integer"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "integer", + oldNullable: true); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 8a99b3aa9a..349028da7e 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -316,7 +316,7 @@ namespace Bit.PostgresMigrations.Migrations b.Property("CreationDate") .HasColumnType("timestamp with time zone"); - b.Property("EventType") + b.Property("EventType") .HasColumnType("integer"); b.Property("Filters") diff --git a/util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs b/util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs new file mode 100644 index 0000000000..d3b053de92 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.Designer.cs @@ -0,0 +1,3252 @@ +// +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("20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration")] + partial class AllowNullEventTypeOrganizationIntegrationConfiguration + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.cs b/util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.cs new file mode 100644 index 0000000000..9eef243d73 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250716205135_AllowNullEventTypeOrganizationIntegrationConfiguration.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AllowNullEventTypeOrganizationIntegrationConfiguration : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "EventType", + table: "OrganizationIntegrationConfiguration", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index d10e5df4cf..62fcd433aa 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -308,7 +308,7 @@ namespace Bit.SqliteMigrations.Migrations b.Property("CreationDate") .HasColumnType("TEXT"); - b.Property("EventType") + b.Property("EventType") .HasColumnType("INTEGER"); b.Property("Filters") From 8a5823bff73763c02d334c46f724df45757cbf3d Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:57:58 +0100 Subject: [PATCH 075/326] [PM 18701]Optional payment modal after signup (#6014) * Add endpoint to swap plan frequency * Add endpoint to swap plan frequency * Resolve pr comments Signed-off-by: Cy Okeke * Refactor the code Signed-off-by: Cy Okeke * Refactor for thr update change frequency * Add Automatic modal opening * catch for organization paying with PayPal --------- Signed-off-by: Cy Okeke --- .../OrganizationBillingController.cs | 31 +++++++++ .../Requests/ChangePlanFrequencyRequest.cs | 10 +++ .../OrganizationWarningsQuery.cs | 15 ++++ src/Core/Billing/Constants/StripeConstants.cs | 7 ++ .../Services/IOrganizationBillingService.cs | 12 ++++ .../Services/OrganizationBillingService.cs | 68 +++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 src/Api/Billing/Models/Requests/ChangePlanFrequencyRequest.cs diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 50a302f6d2..b9db8d81f9 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -382,4 +382,35 @@ public class OrganizationBillingController( return TypedResults.Ok(response); } + + + [HttpPost("change-frequency")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task ChangePlanSubscriptionFrequencyAsync( + [FromRoute] Guid organizationId, + [FromBody] ChangePlanFrequencyRequest request) + { + if (!await currentContext.EditSubscription(organizationId)) + { + return Error.Unauthorized(); + } + + var organization = await organizationRepository.GetByIdAsync(organizationId); + + if (organization == null) + { + return Error.NotFound(); + } + + if (organization.PlanType == request.NewPlanType) + { + return Error.BadRequest("Organization is already on the requested plan frequency."); + } + + await organizationBillingService.UpdateSubscriptionPlanFrequency( + organization, + request.NewPlanType); + + return TypedResults.Ok(); + } } diff --git a/src/Api/Billing/Models/Requests/ChangePlanFrequencyRequest.cs b/src/Api/Billing/Models/Requests/ChangePlanFrequencyRequest.cs new file mode 100644 index 0000000000..88fff85cb3 --- /dev/null +++ b/src/Api/Billing/Models/Requests/ChangePlanFrequencyRequest.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; + +namespace Bit.Api.Billing.Models.Requests; + +public class ChangePlanFrequencyRequest +{ + [Required] + public PlanType NewPlanType { get; set; } +} diff --git a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs index f6a0e5b1e6..7fbdf3c2b0 100644 --- a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs +++ b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs @@ -12,6 +12,7 @@ using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; using Stripe; +using static Bit.Core.Billing.Utilities; using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning; using InactiveSubscriptionWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning; @@ -100,6 +101,20 @@ public class OrganizationWarningsQuery( Provider? provider, Subscription subscription) { + if (organization.Enabled && subscription.Status is StripeConstants.SubscriptionStatus.Trialing) + { + var isStripeCustomerWithoutPayment = + subscription.Customer.InvoiceSettings.DefaultPaymentMethodId is null; + var isBraintreeCustomer = + subscription.Customer.Metadata.ContainsKey(BraintreeCustomerIdKey); + var hasNoPaymentMethod = isStripeCustomerWithoutPayment && !isBraintreeCustomer; + + if (hasNoPaymentMethod && await currentContext.OrganizationOwner(organization.Id)) + { + return new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" }; + } + } + if (organization.Enabled || subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid and not StripeConstants.SubscriptionStatus.Canceled) diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 6ecfb4d28b..7b4cb3baed 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -118,4 +118,11 @@ public static class StripeConstants public const string Deferred = "deferred"; public const string Immediately = "immediately"; } + + public static class MissingPaymentMethodBehaviorOptions + { + public const string CreateInvoice = "create_invoice"; + public const string Cancel = "cancel"; + public const string Pause = "pause"; + } } diff --git a/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs index f35bafdd29..d34bd86e7b 100644 --- a/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/IOrganizationBillingService.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Tax.Models; @@ -44,4 +45,15 @@ public interface IOrganizationBillingService Organization organization, TokenizedPaymentSource tokenizedPaymentSource, TaxInformation taxInformation); + + /// + /// Updates the subscription with new plan frequencies and changes the collection method to charge_automatically if a valid payment method exists. + /// Validates that the customer has a payment method attached before switching to automatic charging. + /// Handles both Password Manager and Secrets Manager subscription items separately to ensure billing interval compatibility. + /// + /// The Organization whose subscription to update. + /// The Stripe price/plan for the new Password Manager and secrets manager. + /// Thrown when the is . + /// Thrown when no payment method is found for the customer, no plan IDs are provided, or subscription update fails. + Task UpdateSubscriptionPlanFrequency(Organization organization, PlanType newPlanType); } diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 3fce618500..f32e835dbf 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -145,6 +145,55 @@ public class OrganizationBillingService( { await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource); await subscriberService.UpdateTaxInformation(organization, taxInformation); + await UpdateMissingPaymentMethodBehaviourAsync(organization); + } + } + + public async Task UpdateSubscriptionPlanFrequency( + Organization organization, PlanType newPlanType) + { + ArgumentNullException.ThrowIfNull(organization); + + var subscription = await subscriberService.GetSubscriptionOrThrow(organization); + var subscriptionItems = subscription.Items.Data; + + var newPlan = await pricingClient.GetPlanOrThrow(newPlanType); + var oldPlan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + // Build the subscription update options + var subscriptionItemOptions = new List(); + foreach (var item in subscriptionItems) + { + var subscriptionItemOption = new SubscriptionItemOptions + { + Id = item.Id, + Quantity = item.Quantity, + Price = item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId ? newPlan.SecretsManager.StripeSeatPlanId : newPlan.PasswordManager.StripeSeatPlanId + }; + + subscriptionItemOptions.Add(subscriptionItemOption); + } + var updateOptions = new SubscriptionUpdateOptions + { + Items = subscriptionItemOptions, + ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations + }; + + try + { + // Update the subscription in Stripe + await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, updateOptions); + organization.PlanType = newPlan.Type; + await organizationRepository.ReplaceAsync(organization); + } + catch (StripeException stripeException) + { + logger.LogError(stripeException, "Failed to update subscription plan for subscriber ({SubscriberID}): {Error}", + organization.Id, stripeException.Message); + + throw new BillingException( + message: "An error occurred while updating the subscription plan", + innerException: stripeException); } } @@ -545,5 +594,24 @@ public class OrganizationBillingService( return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any(); } + private async Task UpdateMissingPaymentMethodBehaviourAsync(Organization organization) + { + var subscription = await subscriberService.GetSubscriptionOrThrow(organization); + if (subscription.TrialSettings?.EndBehavior?.MissingPaymentMethod == StripeConstants.MissingPaymentMethodBehaviorOptions.Cancel) + { + var options = new SubscriptionUpdateOptions + { + TrialSettings = new SubscriptionTrialSettingsOptions + { + EndBehavior = new SubscriptionTrialSettingsEndBehaviorOptions + { + MissingPaymentMethod = StripeConstants.MissingPaymentMethodBehaviorOptions.CreateInvoice + } + } + }; + await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, options); + } + } + #endregion } From 04031a94c2c509157b1030920d4755c475dec136 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:08:20 -0500 Subject: [PATCH 076/326] [PM-23804] Add logging to `AdjustSeatsAsync` to identify Stripe/Organization seat disrecpancy (#6098) * Add logging for seat scale * Run dotnet format --- .../Implementations/OrganizationService.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 4f25f5fc53..84fb9532dc 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -324,8 +324,14 @@ public class OrganizationService : IOrganizationService throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats."); } + _logger.LogInformation("{Method}: Invoking _paymentService.AdjustSeatsAsync with {AdditionalSeats} additional seats for Organization ({OrganizationID})", + nameof(AdjustSeatsAsync), additionalSeats, organization.Id); + var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats); organization.Seats = (short?)newSeatTotal; + + _logger.LogInformation("{Method}: Invoking _organizationRepository.ReplaceAsync with {Seats} seats for Organization ({OrganizationID})", nameof(AdjustSeatsAsync), organization.Seats, organization.Id); ; + await ReplaceAndUpdateCacheAsync(organization); if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && @@ -1190,12 +1196,20 @@ public class OrganizationService : IOrganizationService public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null) { - await _organizationRepository.ReplaceAsync(org); - await _applicationCacheService.UpsertOrganizationAbilityAsync(org); - - if (orgEvent.HasValue) + try { - await _eventService.LogOrganizationEventAsync(org, orgEvent.Value); + await _organizationRepository.ReplaceAsync(org); + await _applicationCacheService.UpsertOrganizationAbilityAsync(org); + + if (orgEvent.HasValue) + { + await _eventService.LogOrganizationEventAsync(org, orgEvent.Value); + } + } + catch (Exception exception) + { + _logger.LogError(exception, "An error occurred while calling {Method} for Organization ({OrganizationID})", nameof(ReplaceAndUpdateCacheAsync), org.Id); + throw; } } From 6278fe7bc547361d10f39c72eeaae86e9459b29e Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Tue, 22 Jul 2025 13:58:26 -0400 Subject: [PATCH 077/326] Removing the unused ciritcal and notification feature flags for dirt (#6068) --- src/Core/Constants.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d31d141431..070ae6baf1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -158,10 +158,6 @@ public static class FeatureFlagKeys public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; - /* Data Insights and Reporting Team */ - public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; - public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications"; - /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; From 947ae8db516bef48c4a9da31aea1ff6c513e58ad Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 22 Jul 2025 17:30:25 -0400 Subject: [PATCH 078/326] [PM-19145] refactor organization service.import async (#5800) * initial lift and shift * extract function RemoveExistingExternalUsers * Extract function RemoveExistingUsers() * extract function OverwriteExisting() * create new model for sync data * extract add users to function, rename * rename OrganizatinUserInvite for command, implement command * implement command * refactor groups logic * fix imports * remove old tests, fix imports * fix namespace * fix CommandResult useage * tests wip * wip * wip * remove redundant code, remove looping db call, refactor tests * clean up * remove looping db call with bulk method * clean up * remove orgId param to use id already in request * change param * cleanup params * remove IReferenceEventService * fix test * fix tests * cr feedback * remove _timeProvider * add xmldoc, refactor to make InviteOrganizationUsersCommand vNext instead of default * switch back to command * re-add old ImportAsync impl * fix test * add feature flag * cleanup * clean up * fix tests * wip * wip * add api integration tests for users WIP * groups integration tests * cleanup * fix error from merging main * fix tests * cr feedback * fix test * fix test --- .../src/Scim/Models/ScimUserRequestModel.cs | 4 +- .../Controllers/OrganizationController.cs | 38 +- ...IImportOrganizationUserAndGroupsCommand.cs | 16 + ...ImportOrganizationUsersAndGroupsCommand.cs | 391 ++++++++++++++++++ .../Import/OrganizationGroupImportData.cs | 41 ++ .../Import/OrganizationUserImportData.cs | 32 ++ .../IInviteOrganizationUsersCommand.cs | 10 + .../InviteOrganizationUsersCommand.cs | 38 +- .../CreateOrganizationUserExtensions.cs | 2 +- .../Models/InviteOrganizationUsersRequest.cs | 4 +- ...nviteOrganizationUsersValidationRequest.cs | 2 +- ... => OrganizationUserInviteCommandModel.cs} | 8 +- .../InviteOrganizationUserValidator.cs | 3 +- src/Core/Constants.cs | 1 + ...OrganizationServiceCollectionExtensions.cs | 1 + ...tOrganizationUsersAndGroupsCommandTests.cs | 312 ++++++++++++++ .../Helpers/OrganizationTestHelpers.cs | 18 + .../InviteOrganizationUsersRequestTests.cs | 6 +- ...tOrganizationUsersAndGroupsCommandTests.cs | 215 ++++++++++ .../InviteOrganizationUserCommandTests.cs | 25 +- .../InviteOrganizationUsersValidatorTests.cs | 19 +- 21 files changed, 1137 insertions(+), 49 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Import/IImportOrganizationUserAndGroupsCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationGroupImportData.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationUserImportData.cs rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/{OrganizationUserInvite.cs => OrganizationUserInviteCommandModel.cs} (88%) create mode 100644 test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs diff --git a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs index 0baf6469ff..fc4f781e42 100644 --- a/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs +++ b/bitwarden_license/src/Scim/Models/ScimUserRequestModel.cs @@ -6,9 +6,9 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Utilities; -using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite; namespace Bit.Scim.Models; @@ -47,7 +47,7 @@ public class ScimUserRequestModel : BaseScimUserModel return new InviteOrganizationUsersRequest( invites: [ - new Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: email, externalId: ExternalIdForInvite() ) diff --git a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs index a1af1c3fb8..18afa10ac0 100644 --- a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs +++ b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs @@ -4,8 +4,9 @@ using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.Models.Public.Response; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Context; -using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Settings; @@ -21,15 +22,21 @@ public class OrganizationController : Controller private readonly IOrganizationService _organizationService; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; + private readonly IImportOrganizationUsersAndGroupsCommand _importOrganizationUsersAndGroupsCommand; + private readonly IFeatureService _featureService; public OrganizationController( IOrganizationService organizationService, ICurrentContext currentContext, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IImportOrganizationUsersAndGroupsCommand importOrganizationUsersAndGroupsCommand, + IFeatureService featureService) { _organizationService = organizationService; _currentContext = currentContext; _globalSettings = globalSettings; + _importOrganizationUsersAndGroupsCommand = importOrganizationUsersAndGroupsCommand; + _featureService = featureService; } /// @@ -50,13 +57,26 @@ public class OrganizationController : Controller throw new BadRequestException("You cannot import this much data at once."); } - await _organizationService.ImportAsync( - _currentContext.OrganizationId.Value, - model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), - model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), - model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), - model.OverwriteExisting.GetValueOrDefault(), - EventSystemUser.PublicApi); + if (_featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor)) + { + await _importOrganizationUsersAndGroupsCommand.ImportAsync( + _currentContext.OrganizationId.Value, + model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), + model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), + model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), + model.OverwriteExisting.GetValueOrDefault()); + } + else + { + await _organizationService.ImportAsync( + _currentContext.OrganizationId.Value, + model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), + model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), + model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), + model.OverwriteExisting.GetValueOrDefault(), + Core.Enums.EventSystemUser.PublicApi); + } + return new OkResult(); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/IImportOrganizationUserAndGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/IImportOrganizationUserAndGroupsCommand.cs new file mode 100644 index 0000000000..b74da0a2e8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/IImportOrganizationUserAndGroupsCommand.cs @@ -0,0 +1,16 @@ +#nullable enable + +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.Models.Business; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IImportOrganizationUsersAndGroupsCommand +{ + Task ImportAsync(Guid organizationId, + IEnumerable groups, + IEnumerable newUsers, + IEnumerable removeUserExternalIds, + bool overwriteExisting + ); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs new file mode 100644 index 0000000000..89288eb4ba --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs @@ -0,0 +1,391 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Pricing; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersAndGroupsCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IPaymentService _paymentService; + private readonly IGroupRepository _groupRepository; + private readonly IEventService _eventService; + private readonly ICurrentContext _currentContext; + private readonly IOrganizationService _organizationService; + private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand; + private readonly IPricingClient _pricingClient; + + private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi; + + public ImportOrganizationUsersAndGroupsCommand(IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IPaymentService paymentService, + IGroupRepository groupRepository, + IEventService eventService, + ICurrentContext currentContext, + IOrganizationService organizationService, + IInviteOrganizationUsersCommand inviteOrganizationUsersCommand, + IPricingClient pricingClient + ) + { + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _paymentService = paymentService; + _groupRepository = groupRepository; + _eventService = eventService; + _currentContext = currentContext; + _organizationService = organizationService; + _inviteOrganizationUsersCommand = inviteOrganizationUsersCommand; + _pricingClient = pricingClient; + } + + /// + /// Imports and synchronizes organization users and groups. + /// + /// The unique identifier of the organization. + /// List of groups to import. + /// List of users to import. + /// A collection of ExternalUserIds to be removed from the organization. + /// Indicates whether to delete existing external users from the organization + /// who are not included in the current import. + /// Thrown if the organization does not exist. + /// Thrown if the organization is not configured to use directory syncing. + public async Task ImportAsync(Guid organizationId, + IEnumerable importedGroups, + IEnumerable importedUsers, + IEnumerable removeUserExternalIds, + bool overwriteExisting) + { + var organization = await GetOrgById(organizationId); + if (organization is null) + { + throw new NotFoundException(); + } + + if (!organization.UseDirectory) + { + throw new BadRequestException("Organization cannot use directory syncing."); + } + + var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); + var importUserData = new OrganizationUserImportData(existingUsers, importedUsers); + var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>(); + + await RemoveExistingExternalUsers(removeUserExternalIds, events, importUserData); + + if (overwriteExisting) + { + await OverwriteExisting(events, importUserData); + } + + await UpdateExistingUsers(importedUsers, importUserData); + + await AddNewUsers(organization, importedUsers, importUserData); + + await ImportGroups(organization, importedGroups, importUserData); + + await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, _EventSystemUser, e.d))); + } + + /// + /// Deletes external users based on provided set of ExternalIds. + /// + /// A collection of external user IDs to be deleted. + /// A list to which user removal events will be added. + /// Data containing imported and existing external users. + + private async Task RemoveExistingExternalUsers(IEnumerable removeUserExternalIds, + List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, + OrganizationUserImportData importUserData) + { + if (!removeUserExternalIds.Any()) + { + return; + } + + var existingUsersDict = importUserData.ExistingExternalUsers.ToDictionary(u => u.ExternalId); + // Determine which ids in removeUserExternalIds to delete based on: + // They are not in ImportedExternalIds, they are in existingUsersDict, and they are not an owner. + var removeUsersSet = new HashSet(removeUserExternalIds) + .Except(importUserData.ImportedExternalIds) + .Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner) + .Select(u => existingUsersDict[u]); + + await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id)); + events.AddRange(removeUsersSet.Select(u => ( + u, + EventType.OrganizationUser_Removed, + (DateTime?)DateTime.UtcNow + )) + ); + } + + /// + /// Updates existing organization users by assigning each an ExternalId from the imported user data + /// where a match is found by email and the existing user lacks an ExternalId. Saves the updated + /// users and updates the ExistingExternalUsersIdDict mapping. + /// + /// List of imported organization users. + /// Data containing existing and imported users, along with mapping dictionaries. + private async Task UpdateExistingUsers(IEnumerable importedUsers, OrganizationUserImportData importUserData) + { + if (!importedUsers.Any()) + { + return; + } + + var updateUsers = new List(); + + // Map existing and imported users to dicts keyed by Email + var existingUsersEmailsDict = importUserData.ExistingUsers + .Where(u => string.IsNullOrWhiteSpace(u.ExternalId)) + .ToDictionary(u => u.Email); + var importedUsersEmailsDict = importedUsers.ToDictionary(u => u.Email); + + // Determine which users to update. + var userEmailsToUpdate = existingUsersEmailsDict.Keys.Intersect(importedUsersEmailsDict.Keys).ToList(); + var userIdsToUpdate = userEmailsToUpdate.Select(e => existingUsersEmailsDict[e].Id).ToList(); + + var organizationUsers = (await _organizationUserRepository.GetManyAsync(userIdsToUpdate)).ToDictionary(u => u.Id); + + foreach (var userEmail in userEmailsToUpdate) + { + // verify userEmail has an associated OrganizationUser + existingUsersEmailsDict.TryGetValue(userEmail, out var existingUser); + organizationUsers.TryGetValue(existingUser!.Id, out var organizationUser); + importedUsersEmailsDict.TryGetValue(userEmail, out var importedUser); + + if (organizationUser is null || importedUser is null) + { + continue; + } + + organizationUser.ExternalId = importedUser.ExternalId; + updateUsers.Add(organizationUser); + importUserData.ExistingExternalUsersIdDict.Add(organizationUser.ExternalId, organizationUser.Id); + } + await _organizationUserRepository.UpsertManyAsync(updateUsers); + } + + /// + /// Adds new external users to the organization by inviting users who are present in the imported data + /// but not already part of the organization. Sends invitations, updates the user Id mapping on success, + /// and throws exceptions on failure. + /// + /// The target organization to which users are being added. + /// A collection of imported users to consider for addition. + /// Data containing imported user info and existing user mappings. + private async Task AddNewUsers(Organization organization, + IEnumerable importedUsers, + OrganizationUserImportData importUserData) + { + // Determine which users are already in the organization + var existingUsersSet = new HashSet(importUserData.ExistingExternalUsersIdDict.Keys); + var usersToAdd = importUserData.ImportedExternalIds.Except(existingUsersSet).ToList(); + var userInvites = new List<(OrganizationUserInvite, string)>(); + var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization); + + foreach (var user in importedUsers) + { + if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email)) + { + continue; + } + + try + { + var invite = new OrganizationUserInvite + { + Emails = new List { user.Email }, + Type = OrganizationUserType.User, + Collections = new List(), + AccessSecretsManager = hasStandaloneSecretsManager + }; + userInvites.Add((invite, user.ExternalId)); + } + catch (BadRequestException) + { + // Thrown when the user is already invited to the organization + continue; + } + } + + var invitedUsers = await _organizationService.InviteUsersAsync(organization.Id, Guid.Empty, _EventSystemUser, userInvites); + foreach (var invitedUser in invitedUsers) + { + importUserData.ExistingExternalUsersIdDict.TryAdd(invitedUser.ExternalId!, invitedUser.Id); + } + } + + /// + /// Deletes existing external users from the organization who are not included in the current import and are not owners. + /// Records corresponding removal events and updates the internal mapping by removing deleted users. + /// + /// A list to which user removal events will be added. + /// Data containing existing and imported external users along with their Id mappings. + private async Task OverwriteExisting( + List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, + OrganizationUserImportData importUserData) + { + var usersToDelete = importUserData.ExistingExternalUsers.Where(u => + u.Type != OrganizationUserType.Owner && + !importUserData.ImportedExternalIds.Contains(u.ExternalId) && + importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId)); + await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); + events.AddRange(usersToDelete.Select(u => ( + u, + EventType.OrganizationUser_Removed, + (DateTime?)DateTime.UtcNow + )) + ); + foreach (var deletedUser in usersToDelete) + { + importUserData.ExistingExternalUsersIdDict.Remove(deletedUser.ExternalId); + } + } + + /// + /// Imports group data into the organization by saving new groups and updating existing ones. + /// + /// The organization into which groups are being imported. + /// A collection of groups to be imported. + /// Data containing information about existing and imported users. + private async Task ImportGroups(Organization organization, IEnumerable importedGroups, OrganizationUserImportData importUserData) + { + if (!importedGroups.Any()) + { + return; + } + + if (!organization.UseGroups) + { + throw new BadRequestException("Organization cannot use groups."); + } + + var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); + var importGroupData = new OrganizationGroupImportData(importedGroups, existingGroups); + + await SaveNewGroups(importGroupData, importUserData); + await UpdateExistingGroups(importGroupData, importUserData, organization); + } + + /// + /// Saves newly imported groups that do not already exist in the organization. + /// Sets their creation and revision dates, associates users with each group. + /// + /// Data containing both imported and existing groups. + /// Data containing information about existing and imported users. + private async Task SaveNewGroups(OrganizationGroupImportData importGroupData, OrganizationUserImportData importUserData) + { + var existingExternalGroupsDict = importGroupData.ExistingExternalGroups.ToDictionary(g => g.ExternalId!); + var newGroups = importGroupData.Groups + .Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId!)) + .Select(g => g.Group) + .ToList()!; + + var savedGroups = new List(); + foreach (var group in newGroups) + { + group.CreationDate = group.RevisionDate = DateTime.UtcNow; + + savedGroups.Add(await _groupRepository.CreateAsync(group)); + await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds, + importUserData.ExistingExternalUsersIdDict); + } + + await _eventService.LogGroupEventsAsync( + savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow))); + } + + /// + /// Updates existing groups in the organization based on imported group data. + /// If a group's name has changed, it updates the name and revision date in the repository. + /// Also updates group-user associations. + /// + /// Data containing imported groups and their user associations. + /// Data containing imported and existing organization users. + /// The organization to which the groups belong. + private async Task UpdateExistingGroups(OrganizationGroupImportData importGroupData, + OrganizationUserImportData importUserData, + Organization organization) + { + var updateGroups = importGroupData.ExistingExternalGroups + .Where(g => importGroupData.GroupsDict.ContainsKey(g.ExternalId!)) + .ToList(); + + if (updateGroups.Any()) + { + // get existing group users + var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organization.Id); + var existingGroupUsers = groupUsers + .GroupBy(gu => gu.GroupId) + .ToDictionary(g => g.Key, g => new HashSet(g.Select(gr => gr.OrganizationUserId))); + + foreach (var group in updateGroups) + { + // Check for changes to the group, update if changed. + var updatedGroup = importGroupData.GroupsDict[group.ExternalId!].Group; + if (group.Name != updatedGroup.Name) + { + group.RevisionDate = DateTime.UtcNow; + group.Name = updatedGroup.Name; + + await _groupRepository.ReplaceAsync(group); + } + + // compare and update user group associations + await UpdateUsersAsync(group, importGroupData.GroupsDict[group.ExternalId!].ExternalUserIds, + importUserData.ExistingExternalUsersIdDict, + existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null); + + } + + await _eventService.LogGroupEventsAsync( + updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)_EventSystemUser, (DateTime?)DateTime.UtcNow))); + } + + } + + /// + /// Updates the user associations for a given group. + /// Only updates if the set of associated users differs from the current group membership. + /// Filters users based on those present in the existing user Id dictionary. + /// + /// The group whose user associations are being updated. + /// A set of ExternalUserIds to be associated with the group. + /// A dictionary mapping ExternalUserIds to internal user Ids. + /// Optional set of currently associated user Ids for comparison. + private async Task UpdateUsersAsync(Group group, HashSet groupUsers, + Dictionary existingUsersIdDict, HashSet? existingUsers = null) + { + var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys); + var users = new HashSet(availableUsers.Select(u => existingUsersIdDict[u])); + if (existingUsers is not null && existingUsers.Count == users.Count && users.SetEquals(existingUsers)) + { + return; + } + + await _groupRepository.UpdateUsersAsync(group.Id, users); + } + + private async Task GetOrgById(Guid id) + { + return await _organizationRepository.GetByIdAsync(id); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationGroupImportData.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationGroupImportData.cs new file mode 100644 index 0000000000..6f49cb82e6 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationGroupImportData.cs @@ -0,0 +1,41 @@ +#nullable enable + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business; + +namespace Bit.Core.Models.Data.Organizations; + +/// +/// Represents the data required to import organization groups, +/// including newly imported groups and existing groups within the organization. +/// +public class OrganizationGroupImportData +{ + /// + /// The collection of groups that are being imported. + /// + public readonly IEnumerable Groups; + + /// + /// Collection of groups that already exist in the organization. + /// + public readonly ICollection ExistingGroups; + + /// + /// Existing groups with ExternalId set. + /// + public readonly IEnumerable ExistingExternalGroups; + + /// + /// Mapping of imported groups keyed by their ExternalId. + /// + public readonly IDictionary GroupsDict; + + public OrganizationGroupImportData(IEnumerable groups, ICollection existingGroups) + { + Groups = groups; + GroupsDict = groups.ToDictionary(g => g.Group.ExternalId!); + ExistingGroups = existingGroups; + ExistingExternalGroups = existingGroups.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationUserImportData.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationUserImportData.cs new file mode 100644 index 0000000000..6575afe842 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/OrganizationUserImportData.cs @@ -0,0 +1,32 @@ +#nullable enable + +using Bit.Core.Models.Business; +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers; + +public class OrganizationUserImportData +{ + /// + /// Set of user ExternalIds that are being imported + /// + public readonly HashSet ImportedExternalIds; + /// + /// All existing OrganizationUsers for the organization + /// + public readonly ICollection ExistingUsers; + /// + /// Existing OrganizationUsers with ExternalIds set. + /// + public readonly IEnumerable ExistingExternalUsers; + /// + /// Mapping of an existing users's ExternalId to their Id + /// + public readonly Dictionary ExistingExternalUsersIdDict; + + public OrganizationUserImportData(ICollection existingUsers, IEnumerable importedUsers) + { + ImportedExternalIds = new HashSet(importedUsers?.Select(u => u.ExternalId) ?? new List()); + ExistingUsers = existingUsers; + ExistingExternalUsers = ExistingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); + ExistingExternalUsersIdDict = ExistingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs index 7e0a8dc3cd..7e8fd4c30a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -19,4 +19,14 @@ public interface IInviteOrganizationUsersCommand /// /// Response from InviteScimOrganiation Task> InviteScimOrganizationUserAsync(InviteOrganizationUsersRequest request); + /// + /// Sends invitations to add imported organization users via the public API. + /// This can be a Success or a Failure. Failure will contain the Error along with a representation of the errored value. + /// Success will be the successful return object. + /// + /// + /// Contains the details for inviting the imported organization users. + /// + /// Response from InviteOrganiationUsersAsync + Task> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index addb1997a9..47003be5c6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -12,13 +12,13 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Validation; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.Extensions.Logging; -using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -74,6 +74,40 @@ public class InviteOrganizationUsersCommand(IEventService eventService, } } + public async Task> InviteImportedOrganizationUsersAsync(InviteOrganizationUsersRequest request) + { + var result = await InviteOrganizationUsersAsync(request); + + switch (result) + { + case Failure failure: + return new Failure( + new Error( + failure.Error.Message, + new InviteOrganizationUsersResponse(failure.Error.ErroredValue.InvitedUsers, request.InviteOrganization.OrganizationId) + ) + ); + + case Success success when success.Value.InvitedUsers.Any(): + + List<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events = new List<(OrganizationUser, EventType, EventSystemUser, DateTime?)>(); + foreach (var user in success.Value.InvitedUsers) + { + events.Add((user, EventType.OrganizationUser_Invited, EventSystemUser.PublicApi, request.PerformedAt.UtcDateTime)); + } + + await eventService.LogOrganizationUserEventsAsync(events); + + return new Success(new InviteOrganizationUsersResponse(success.Value.InvitedUsers, request.InviteOrganization.OrganizationId) + ); + + default: + return new Failure( + new InvalidResultTypeError( + new InviteOrganizationUsersResponse(request.InviteOrganization.OrganizationId))); + } + } + private async Task> InviteOrganizationUsersAsync(InviteOrganizationUsersRequest request) { var invitesToSend = (await FilterExistingUsersAsync(request)).ToArray(); @@ -141,7 +175,7 @@ public class InviteOrganizationUsersCommand(IEventService eventService, organizationId: organization!.Id)); } - private async Task> FilterExistingUsersAsync(InviteOrganizationUsersRequest request) + private async Task> FilterExistingUsersAsync(InviteOrganizationUsersRequest request) { var existingEmails = new HashSet(await organizationUserRepository.SelectKnownEmailsAsync( request.InviteOrganization.OrganizationId, request.Invites.Select(i => i.Email), false), diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUserExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUserExtensions.cs index 23c38a51cb..b0f81bd92a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUserExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/CreateOrganizationUserExtensions.cs @@ -7,7 +7,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse public static class CreateOrganizationUserExtensions { - public static CreateOrganizationUser MapToDataModel(this OrganizationUserInvite organizationUserInvite, + public static CreateOrganizationUser MapToDataModel(this OrganizationUserInviteCommandModel organizationUserInvite, DateTimeOffset performedAt, InviteOrganization organization) => new() diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs index 84b350c551..2a54f26eb8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersRequest.cs @@ -4,12 +4,12 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse public class InviteOrganizationUsersRequest { - public OrganizationUserInvite[] Invites { get; } = []; + public OrganizationUserInviteCommandModel[] Invites { get; } = []; public InviteOrganization InviteOrganization { get; } public Guid PerformedBy { get; } public DateTimeOffset PerformedAt { get; } - public InviteOrganizationUsersRequest(OrganizationUserInvite[] invites, + public InviteOrganizationUsersRequest(OrganizationUserInviteCommandModel[] invites, InviteOrganization inviteOrganization, Guid performedBy, DateTimeOffset performedAt) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs index 56812e2617..e2eb91454c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/InviteOrganizationUsersValidationRequest.cs @@ -32,7 +32,7 @@ public class InviteOrganizationUsersValidationRequest SecretsManagerSubscriptionUpdate = smSubscriptionUpdate; } - public OrganizationUserInvite[] Invites { get; init; } = []; + public OrganizationUserInviteCommandModel[] Invites { get; init; } = []; public InviteOrganization InviteOrganization { get; init; } public Guid PerformedBy { get; init; } public DateTimeOffset PerformedAt { get; init; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInviteCommandModel.cs similarity index 88% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInviteCommandModel.cs index 0b83680aa5..4d0f56efe4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInvite.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Models/OrganizationUserInviteCommandModel.cs @@ -7,7 +7,7 @@ using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Invite namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -public class OrganizationUserInvite +public class OrganizationUserInviteCommandModel { public string Email { get; private init; } public CollectionAccessSelection[] AssignedCollections { get; private init; } @@ -17,7 +17,7 @@ public class OrganizationUserInvite public bool AccessSecretsManager { get; private init; } public Guid[] Groups { get; private init; } - public OrganizationUserInvite(string email, string externalId) : + public OrganizationUserInviteCommandModel(string email, string externalId) : this( email: email, assignedCollections: [], @@ -29,7 +29,7 @@ public class OrganizationUserInvite { } - public OrganizationUserInvite(OrganizationUserInvite invite, bool accessSecretsManager) : + public OrganizationUserInviteCommandModel(OrganizationUserInviteCommandModel invite, bool accessSecretsManager) : this(invite.Email, invite.AssignedCollections, invite.Groups, @@ -41,7 +41,7 @@ public class OrganizationUserInvite } - public OrganizationUserInvite(string email, + public OrganizationUserInviteCommandModel(string email, IEnumerable assignedCollections, IEnumerable groups, OrganizationUserType type, diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 557ece2104..a3b1e43a04 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -9,7 +9,6 @@ using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; -using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; @@ -41,7 +40,7 @@ public class InviteOrganizationUsersValidator( request = new InviteOrganizationUsersValidationRequest(request) { Invites = request.Invites - .Select(x => new OrganizationUserInvite(x, accessSecretsManager: true)) + .Select(x => new OrganizationUserInviteCommandModel(x, accessSecretsManager: true)) .ToArray() }; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 070ae6baf1..539ff6d977 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -113,6 +113,7 @@ public static class FeatureFlagKeys public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; + public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; /* Auth Team */ diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index b78a305d31..e28831e0ab 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -190,6 +190,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs new file mode 100644 index 0000000000..5c29b8b1b7 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -0,0 +1,312 @@ +using System.Net; +using Bit.Api.AdminConsole.Public.Models.Request; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Services; +using NSubstitute; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Import; + +public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + private Organization _organization = null!; + private string _ownerEmail = null!; + + public ImportOrganizationUsersAndGroupsCommandTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService((IFeatureService featureService) + => featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor) + .Returns(true)); + _client = _factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + // Create the owner account + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + // Create the organization + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, + ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + + // Authorize with the organization api key + await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task Import_Existing_Organization_User_Succeeds() + { + var (email, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, + OrganizationUserType.User); + + var externalId = Guid.NewGuid().ToString(); + var request = new OrganizationImportRequestModel(); + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = []; + request.Members = [ + new OrganizationImportRequestModel.OrganizationImportMemberRequestModel + { + Email = email, + ExternalId = externalId, + Deleted = false + } + ]; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var organizationUserRepository = _factory.GetService(); + var orgUser = await organizationUserRepository.GetByIdAsync(ou.Id); + + Assert.NotNull(orgUser); + Assert.Equal(ou.Id, orgUser.Id); + Assert.Equal(email, orgUser.Email); + Assert.Equal(OrganizationUserType.User, orgUser.Type); + Assert.Equal(externalId, orgUser.ExternalId); + Assert.Equal(OrganizationUserStatusType.Confirmed, orgUser.Status); + Assert.Equal(_organization.Id, orgUser.OrganizationId); + + } + + [Fact] + public async Task Import_New_Organization_User_Succeeds() + { + var email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(email); + + var externalId = Guid.NewGuid().ToString(); + var request = new OrganizationImportRequestModel(); + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = []; + request.Members = [ + new OrganizationImportRequestModel.OrganizationImportMemberRequestModel + { + Email = email, + ExternalId = externalId, + Deleted = false + } + ]; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var organizationUserRepository = _factory.GetService(); + var orgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, email); + + Assert.NotNull(orgUser); + Assert.Equal(email, orgUser.Email); + Assert.Equal(OrganizationUserType.User, orgUser.Type); + Assert.Equal(externalId, orgUser.ExternalId); + Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status); + Assert.Equal(_organization.Id, orgUser.OrganizationId); + } + + [Fact] + public async Task Import_New_And_Existing_Organization_Users_Succeeds() + { + // Existing organization user + var (existingEmail, ou) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, + OrganizationUserType.User); + var existingExternalId = Guid.NewGuid().ToString(); + + // New organization user + var newEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(newEmail); + var newExternalId = Guid.NewGuid().ToString(); + + var request = new OrganizationImportRequestModel(); + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = []; + request.Members = [ + new OrganizationImportRequestModel.OrganizationImportMemberRequestModel + { + Email = existingEmail, + ExternalId = existingExternalId, + Deleted = false + }, + new OrganizationImportRequestModel.OrganizationImportMemberRequestModel + { + Email = newEmail, + ExternalId = newExternalId, + Deleted = false + } + ]; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var organizationUserRepository = _factory.GetService(); + + // Existing user + var existingOrgUser = await organizationUserRepository.GetByIdAsync(ou.Id); + Assert.NotNull(existingOrgUser); + Assert.Equal(existingEmail, existingOrgUser.Email); + Assert.Equal(OrganizationUserType.User, existingOrgUser.Type); + Assert.Equal(existingExternalId, existingOrgUser.ExternalId); + Assert.Equal(OrganizationUserStatusType.Confirmed, existingOrgUser.Status); + Assert.Equal(_organization.Id, existingOrgUser.OrganizationId); + + // New User + var newOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, newEmail); + Assert.NotNull(newOrgUser); + Assert.Equal(newEmail, newOrgUser.Email); + Assert.Equal(OrganizationUserType.User, newOrgUser.Type); + Assert.Equal(newExternalId, newOrgUser.ExternalId); + Assert.Equal(OrganizationUserStatusType.Invited, newOrgUser.Status); + Assert.Equal(_organization.Id, newOrgUser.OrganizationId); + } + + [Fact] + public async Task Import_Existing_Groups_Succeeds() + { + var organizationUserRepository = _factory.GetService(); + var group = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id); + var request = new OrganizationImportRequestModel(); + var addedMember = new OrganizationImportRequestModel.OrganizationImportMemberRequestModel + { + Email = "test@test.com", + ExternalId = "bwtest-externalId", + Deleted = false + }; + + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = [ + new OrganizationImportRequestModel.OrganizationImportGroupRequestModel + { + Name = "new-name", + ExternalId = "bwtest-externalId", + MemberExternalIds = [] + } + ]; + request.Members = [addedMember]; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var groupRepository = _factory.GetService(); + var existingGroups = (await groupRepository.GetManyByOrganizationIdAsync(_organization.Id)).ToArray(); + + // Assert that we are actually updating the existing group, not adding a new one. + Assert.Single(existingGroups); + Assert.NotNull(existingGroups[0]); + Assert.Equal(group.Id, existingGroups[0].Id); + Assert.Equal("new-name", existingGroups[0].Name); + Assert.Equal(group.ExternalId, existingGroups[0].ExternalId); + + var addedOrgUser = await organizationUserRepository.GetByOrganizationEmailAsync(_organization.Id, addedMember.Email); + Assert.NotNull(addedOrgUser); + } + + [Fact] + public async Task Import_New_Groups_Succeeds() + { + var group = new Group + { + OrganizationId = _organization.Id, + ExternalId = new Guid().ToString(), + Name = "bwtest1" + }; + + var request = new OrganizationImportRequestModel(); + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = [ + new OrganizationImportRequestModel.OrganizationImportGroupRequestModel + { + Name = group.Name, + ExternalId = group.ExternalId, + MemberExternalIds = [] + } + ]; + request.Members = []; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var groupRepository = _factory.GetService(); + var existingGroups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id); + var existingGroup = existingGroups.Where(g => g.ExternalId == group.ExternalId).FirstOrDefault(); + + Assert.NotNull(existingGroup); + Assert.Equal(existingGroup.Name, group.Name); + Assert.Equal(existingGroup.ExternalId, group.ExternalId); + } + + [Fact] + public async Task Import_New_And_Existing_Groups_Succeeds() + { + var existingGroup = await OrganizationTestHelpers.CreateGroup(_factory, _organization.Id); + + var newGroup = new Group + { + OrganizationId = _organization.Id, + ExternalId = "test", + Name = "bwtest1" + }; + + var request = new OrganizationImportRequestModel(); + request.LargeImport = false; + request.OverwriteExisting = false; + request.Groups = [ + new OrganizationImportRequestModel.OrganizationImportGroupRequestModel + { + Name = "new-name", + ExternalId = existingGroup.ExternalId, + MemberExternalIds = [] + }, + new OrganizationImportRequestModel.OrganizationImportGroupRequestModel + { + Name = newGroup.Name, + ExternalId = newGroup.ExternalId, + MemberExternalIds = [] + } + ]; + request.Members = []; + + var response = await _client.PostAsync($"/public/organization/import", JsonContent.Create(request)); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + // Assert against the database values + var groupRepository = _factory.GetService(); + var groups = await groupRepository.GetManyByOrganizationIdAsync(_organization.Id); + + var newGroupInDb = groups.Where(g => g.ExternalId == newGroup.ExternalId).FirstOrDefault(); + Assert.NotNull(newGroupInDb); + Assert.Equal(newGroupInDb.Name, newGroup.Name); + Assert.Equal(newGroupInDb.ExternalId, newGroup.ExternalId); + + var existingGroupInDb = groups.Where(g => g.ExternalId == existingGroup.ExternalId).FirstOrDefault(); + Assert.NotNull(existingGroupInDb); + Assert.Equal(existingGroup.Id, existingGroupInDb.Id); + Assert.Equal("new-name", existingGroupInDb.Name); + Assert.Equal(existingGroup.ExternalId, existingGroupInDb.ExternalId); + } +} diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index f2bc9f4bac..ae4e27267d 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -2,6 +2,7 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -78,6 +79,7 @@ public static class OrganizationTestHelpers Status = userStatusType, ExternalId = null, AccessSecretsManager = accessSecretsManager, + Email = userEmail }; if (permissions != null) @@ -130,4 +132,20 @@ public static class OrganizationTestHelpers await organizationDomainRepository.CreateAsync(verifiedDomain); } + + public static async Task CreateGroup(ApiApplicationFactory factory, Guid organizationId) + { + + var groupRepository = factory.GetService(); + var group = new Group + { + OrganizationId = organizationId, + Id = new Guid(), + ExternalId = "bwtest-externalId", + Name = "bwtest" + }; + + await groupRepository.CreateAsync(group, new List()); + return group; + } } diff --git a/test/Core.Test/AdminConsole/Models/InviteOrganizationUsersRequestTests.cs b/test/Core.Test/AdminConsole/Models/InviteOrganizationUsersRequestTests.cs index 71b2b9766c..a9d1836a9f 100644 --- a/test/Core.Test/AdminConsole/Models/InviteOrganizationUsersRequestTests.cs +++ b/test/Core.Test/AdminConsole/Models/InviteOrganizationUsersRequestTests.cs @@ -16,7 +16,7 @@ public class InviteOrganizationUsersRequestTests public void Constructor_WhenPassedInvalidEmail_ThrowsException(string email, OrganizationUserType type, Permissions permissions, string externalId) { var exception = Assert.Throws(() => - new OrganizationUserInvite(email, [], [], type, permissions, externalId, false)); + new OrganizationUserInviteCommandModel(email, [], [], type, permissions, externalId, false)); Assert.Contains(InvalidEmailErrorMessage, exception.Message); } @@ -33,7 +33,7 @@ public class InviteOrganizationUsersRequestTests }; var exception = Assert.Throws(() => - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: validEmail, assignedCollections: [invalidCollectionConfiguration], groups: [], @@ -51,7 +51,7 @@ public class InviteOrganizationUsersRequestTests const string validEmail = "test@email.com"; var validCollectionConfiguration = new CollectionAccessSelection { Id = Guid.NewGuid(), Manage = true }; - var invite = new OrganizationUserInvite( + var invite = new OrganizationUserInviteCommandModel( email: validEmail, assignedCollections: [validCollectionConfiguration], groups: [], diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs new file mode 100644 index 0000000000..da02fbcf4d --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -0,0 +1,215 @@ +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Fakes; +using NSubstitute; +using Xunit; +using Organization = Bit.Core.AdminConsole.Entities.Organization; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; + +public class ImportOrganizationUsersAndGroupsCommandTests +{ + + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); + + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task OrgImportCallsInviteOrgUserCommand( + SutProvider sutProvider, + Organization org, + List existingUsers, + List importedUsers, + List newGroups) + { + SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers); + + var orgUsers = new List(); + + // fix mocked email format, mock OrganizationUsers. + foreach (var u in importedUsers) + { + u.Email += "@bitwardentest.com"; + orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId }); + } + + importedUsers.Add(new ImportedOrganizationUser + { + Email = existingUsers.First().Email, + ExternalId = existingUsers.First().ExternalId + }); + + + existingUsers.First().Type = OrganizationUserType.Owner; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var organizationUserRepository = sutProvider.GetDependency(); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); + + sutProvider.GetDependency().HasSecretsManagerStandalone(org).Returns(true); + sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers); + sutProvider.GetDependency().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns( + new OrganizationSeatCounts + { + Users = existingUsers.Count, + Sponsored = 0 + }); + sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); + sutProvider.GetDependency().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi, + Arg.Any>()) + .Returns(orgUsers); + + await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List(), false); + + var expectedNewUsersCount = importedUsers.Count - 1; + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency().Received(1) + .UpsertManyAsync(Arg.Is>(users => !users.Any())); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + + // Send Invites + await sutProvider.GetDependency().Received(1). + InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi, + Arg.Is>(invites => invites.Count() == expectedNewUsersCount)); + + // Send events + await sutProvider.GetDependency().Received(1) + .LogOrganizationUserEventsAsync(Arg.Any>()); + } + + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task OrgImportCreateNewUsersAndMarryExistingUser( + SutProvider sutProvider, + Organization org, + List existingUsers, + List importedUsers, + List newGroups) + { + SetupOrganizationConfigForImport(sutProvider, org, existingUsers, importedUsers); + + var orgUsers = new List(); + var reInvitedUser = existingUsers.First(); + // Existing user has no external ID. This will make the SUT call UpsertManyAsync + reInvitedUser.ExternalId = ""; + + // Mock an existing org user for this "existing" user + var reInvitedOrgUser = new OrganizationUser { Email = reInvitedUser.Email, Id = reInvitedUser.Id }; + + // fix email formatting, mock orgUsers to be returned + foreach (var u in existingUsers) + { + u.Email += "@bitwardentest.com"; + orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId }); + } + foreach (var u in importedUsers) + { + u.Email += "@bitwardentest.com"; + orgUsers.Add(new OrganizationUser { Email = u.Email, ExternalId = u.ExternalId }); + } + + // add the existing user to be re-imported + importedUsers.Add(new ImportedOrganizationUser + { + Email = reInvitedUser.Email, + ExternalId = reInvitedUser.Email, + }); + + var expectedNewUsersCount = importedUsers.Count - 1; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var organizationUserRepository = sutProvider.GetDependency(); + SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); + + sutProvider.GetDependency().GetManyAsync(Arg.Any>()) + .Returns(new List([reInvitedOrgUser])); + sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers); + sutProvider.GetDependency().GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns( + new OrganizationSeatCounts + { + Users = existingUsers.Count, + Sponsored = 0 + }); + + sutProvider.GetDependency().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi, + Arg.Any>()) + .Returns(orgUsers); + + await sutProvider.Sut.ImportAsync(org.Id, newGroups, importedUsers, new List(), false); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default, default); + + // Upserted existing user + await sutProvider.GetDependency().Received(1) + .UpsertManyAsync(Arg.Is>(users => users.Count() == 1 && users.First() == reInvitedOrgUser)); + + // Send Invites + await sutProvider.GetDependency().Received(1). + InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi, + Arg.Is>(invites => invites.Count() == expectedNewUsersCount)); + + // Send events + await sutProvider.GetDependency().Received(1) + .LogOrganizationUserEventsAsync(Arg.Any>()); + } + + private void SetupOrganizationConfigForImport( + SutProvider sutProvider, + Organization org, + List existingUsers, + List importedUsers) + { + // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.Create(); + + org.UseDirectory = true; + org.Seats = importedUsers.Count + existingUsers.Count + 1; + } + + // Must set real guids in order for dictionary of guids to not throw aggregate exceptions + private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository) + { + organizationUserRepository.CreateManyAsync(Arg.Any>()).Returns( + info => + { + var orgUsers = info.Arg>(); + foreach (var orgUser in orgUsers) + { + orgUser.Id = Guid.NewGuid(); + } + + return Task.FromResult>(orgUsers.Select(u => u.Id).ToList()); + } + ); + + organizationUserRepository.CreateAsync(Arg.Any(), Arg.Any>()).Returns( + info => + { + var orgUser = info.Arg(); + orgUser.Id = Guid.NewGuid(); + return Task.FromResult(orgUser.Id); + } + ); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index cee801d190..aa803bd0c9 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -30,7 +30,6 @@ using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; using static Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Helpers.InviteUserOrganizationValidationRequestHelpers; -using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -54,7 +53,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -112,7 +111,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: orgUser.Email, assignedCollections: [], groups: [], @@ -182,7 +181,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -257,7 +256,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -334,7 +333,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -411,7 +410,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -492,7 +491,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -566,7 +565,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -669,7 +668,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -768,7 +767,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -863,7 +862,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], @@ -942,7 +941,7 @@ public class InviteOrganizationUserCommandTests var request = new InviteOrganizationUsersRequest( invites: [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: user.Email, assignedCollections: [], groups: [], diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index 7c06e04256..a5b220b94a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -14,7 +14,6 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; -using OrganizationUserInvite = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models.OrganizationUserInvite; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; @@ -36,13 +35,13 @@ public class InviteOrganizationUsersValidatorTests { Invites = [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test@email.com", externalId: "test-external-id"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test2@email.com", externalId: "test-external-id2"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test3@email.com", externalId: "test-external-id3") ], @@ -82,13 +81,13 @@ public class InviteOrganizationUsersValidatorTests { Invites = [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test@email.com", externalId: "test-external-id"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test2@email.com", externalId: "test-external-id2"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test3@email.com", externalId: "test-external-id3") ], @@ -126,13 +125,13 @@ public class InviteOrganizationUsersValidatorTests { Invites = [ - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test@email.com", externalId: "test-external-id"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test2@email.com", externalId: "test-external-id2"), - new OrganizationUserInvite( + new OrganizationUserInviteCommandModel( email: "test3@email.com", externalId: "test-external-id3") ], From acd556d56f4829d7ff59c893abd13465d0fa7643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:04:20 +0100 Subject: [PATCH 079/326] =?UTF-8?q?[PM-21031]=C2=A0Optimize=20GET=20Member?= =?UTF-8?q?s=20endpoint=20performance=20(#5907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new feature flag for Members Get Endpoint Optimization * Add a new version of OrganizationUser_ReadByOrganizationIdWithClaimedDomains that uses CTE for better performance * Add stored procedure OrganizationUserUserDetails_ReadByOrganizationId_V2 for retrieving user details, group associations, and collection associations by organization ID. * Add the sql migration script to add the new stored procedures * Introduce GetManyDetailsByOrganizationAsync_vNext and GetManyByOrganizationWithClaimedDomainsAsync_vNext in IOrganizationUserRepository to enhance performance by reducing database round trips. * Updated GetOrganizationUsersClaimedStatusQuery to use an optimized query when the feature flag is enabled * Updated OrganizationUserUserDetailsQuery to use optimized queries when the feature flag is enabled * Add integration tests for GetManyDetailsByOrganizationAsync_vNext * Add integration tests for GetManyByOrganizationWithClaimedDomainsAsync_vNext to validate behavior with verified and unverified domains. * Optimize performance by conditionally setting permissions only for Custom user types in OrganizationUserUserDetailsQuery. * Create UserEmailDomainView to extract email domains from users' email addresses * Create stored procedure Organization_ReadByClaimedUserEmailDomain_V2 that uses UserEmailDomainView to fetch Email domains * Add GetByVerifiedUserEmailDomainAsync_vNext method to IOrganizationRepository and its implementations * Refactor OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 stored procedure to use UserEmailDomainView for email domain extraction, improving query efficiency and clarity. * Enhance IOrganizationUserRepository with detailed documentation for GetManyDetailsByOrganizationAsync method, clarifying its purpose and performance optimizations. Added remarks for better understanding of its functionality. * Fix missing newline at the end of Organization_ReadByClaimedUserEmailDomain_V2.sql to adhere to coding standards. * Update the database migration script to include UserEmailDomainView * Bumped the date on the migration script * Remove GetByVerifiedUserEmailDomainAsync_vNext method and its stored procedure. * Refactor UserEmailDomainView index creation to check for existence before creation * Update OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 to use CTE and add indexes * Remove creation of unique clustered index from UserEmailDomainView and related migration script adjustments * Update indexes and sproc * Fix index name when checking if it already exists * Bump up date on migration script --- .../GetOrganizationUsersClaimedStatusQuery.cs | 9 +- .../OrganizationUserUserDetailsQuery.cs | 83 +++- .../IOrganizationUserRepository.cs | 11 +- src/Core/Constants.cs | 1 + .../OrganizationUserRepository.cs | 75 ++++ .../OrganizationUserRepository.cs | 56 +++ ...serUserDetails_ReadByOrganizationId_V2.sql | 31 ++ ...dByOrganizationIdWithClaimedDomains_V2.sql | 27 ++ src/Sql/dbo/Tables/OrganizationDomain.sql | 8 +- src/Sql/dbo/Tables/OrganizationUser.sql | 7 + src/Sql/dbo/Tables/User.sql | 4 + src/Sql/dbo/Views/UserEmailDomainView.sql | 10 + .../OrganizationUserRepositoryTests.cs | 425 +++++++++++++++++- ...25-07-22_00_OrgUsersQueryOptimizations.sql | 98 ++++ 14 files changed, 836 insertions(+), 9 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.sql create mode 100644 src/Sql/dbo/Views/UserEmailDomainView.sql create mode 100644 util/Migrator/DbScripts/2025-07-22_00_OrgUsersQueryOptimizations.sql diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs index d8c510119a..b27da2a22e 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs @@ -8,13 +8,16 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim { private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IFeatureService _featureService; public GetOrganizationUsersClaimedStatusQuery( IApplicationCacheService applicationCacheService, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + IFeatureService featureService) { _applicationCacheService = applicationCacheService; _organizationUserRepository = organizationUserRepository; + _featureService = featureService; } public async Task> GetUsersOrganizationClaimedStatusAsync(Guid organizationId, IEnumerable organizationUserIds) @@ -27,7 +30,9 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim if (organizationAbility is { Enabled: true, UseOrganizationDomains: true }) { // Get all organization users with claimed domains by the organization - var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); + var organizationUsersWithClaimedDomain = _featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization) + ? await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organizationId) + : await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); // Create a dictionary with the OrganizationUserId and a boolean indicating if the user is claimed by the organization return organizationUserIds.ToDictionary(ouId => ouId, ouId => organizationUsersWithClaimedDomain.Any(ou => ou.Id == ouId)); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs index 587e04826b..aa2cd2df8f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -43,9 +44,12 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return organizationUsers .Select(o => { - var userPermissions = o.GetPermissions(); - - o.Permissions = CoreHelpers.ClassToJsonData(userPermissions); + // Only set permissions for Custom user types for performance optimization + if (o.Type == OrganizationUserType.Custom) + { + var userPermissions = o.GetPermissions(); + o.Permissions = CoreHelpers.ClassToJsonData(userPermissions); + } return o; }); @@ -59,6 +63,11 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer /// List of OrganizationUserUserDetails public async Task> Get(OrganizationUserUserDetailsQueryRequest request) { + if (_featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization)) + { + return await Get_vNext(request); + } + var organizationUsers = await GetOrganizationUserUserDetails(request); var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); @@ -77,6 +86,11 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer /// List of OrganizationUserUserDetails public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) { + if (_featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization)) + { + return await GetAccountRecoveryEnrolledUsers_vNext(request); + } + var organizationUsers = (await GetOrganizationUserUserDetails(request)) .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)); @@ -88,4 +102,65 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return responses; } + private async Task> Get_vNext(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = await _organizationUserRepository + .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections); + + var twoFactorTask = _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); + var claimedStatusTask = _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + + await Task.WhenAll(twoFactorTask, claimedStatusTask); + + var organizationUsersTwoFactorEnabled = twoFactorTask.Result.ToDictionary(u => u.user.Id, u => u.twoFactorIsEnabled); + var organizationUsersClaimedStatus = claimedStatusTask.Result; + var responses = organizationUsers.Select(organizationUserDetails => + { + // Only set permissions for Custom user types for performance optimization + if (organizationUserDetails.Type == OrganizationUserType.Custom) + { + var organizationUserPermissions = organizationUserDetails.GetPermissions(); + organizationUserDetails.Permissions = CoreHelpers.ClassToJsonData(organizationUserPermissions); + } + + var userHasTwoFactorEnabled = organizationUsersTwoFactorEnabled[organizationUserDetails.Id]; + var userIsClaimedByOrganization = organizationUsersClaimedStatus[organizationUserDetails.Id]; + + return (organizationUserDetails, userHasTwoFactorEnabled, userIsClaimedByOrganization); + }); + + return responses; + } + + private async Task> GetAccountRecoveryEnrolledUsers_vNext(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = (await _organizationUserRepository + .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections)) + .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)) + .ToArray(); + + var twoFactorTask = _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers); + var claimedStatusTask = _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + + await Task.WhenAll(twoFactorTask, claimedStatusTask); + + var organizationUsersTwoFactorEnabled = twoFactorTask.Result.ToDictionary(u => u.user.Id, u => u.twoFactorIsEnabled); + var organizationUsersClaimedStatus = claimedStatusTask.Result; + var responses = organizationUsers.Select(organizationUserDetails => + { + // Only set permissions for Custom user types for performance optimization + if (organizationUserDetails.Type == OrganizationUserType.Custom) + { + var organizationUserPermissions = organizationUserDetails.GetPermissions(); + organizationUserDetails.Permissions = CoreHelpers.ClassToJsonData(organizationUserPermissions); + } + + var userHasTwoFactorEnabled = organizationUsersTwoFactorEnabled[organizationUserDetails.Id]; + var userIsClaimedByOrganization = organizationUsersClaimedStatus[organizationUserDetails.Id]; + + return (organizationUserDetails, userHasTwoFactorEnabled, userIsClaimedByOrganization); + }); + + return responses; + } } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index cbdf3913cc..7187ab50a6 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -36,6 +36,12 @@ public interface IOrganizationUserRepository : IRepositoryWhether to include collections /// A list of OrganizationUserUserDetails Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); + /// + /// + /// This method is optimized for performance. + /// Reduces database round trips by fetching all data in fewer queries. + /// + Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); Task GetDetailsByUserAsync(Guid userId, Guid organizationId, @@ -70,7 +76,10 @@ public interface IOrganizationUserRepository : IRepository Task> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId); - + /// + /// Optimized version of with better performance. + /// + Task> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId); Task RevokeManyByIdAsync(IEnumerable organizationUserIds); /// diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 539ff6d977..f040dcc9e8 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,6 +115,7 @@ public static class FeatureFlagKeys public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; + public const string MembersGetEndpointOptimization = "pm-21031-members-get-endpoint-optimization"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index feecf4a5d1..2b9298a75a 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -268,6 +268,68 @@ public class OrganizationUserRepository : Repository, IO } } + public async Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeCollections) + { + using (var connection = new SqlConnection(ConnectionString)) + { + // Use a single call that returns multiple result sets + var results = await connection.QueryMultipleAsync( + "[dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2]", + new + { + OrganizationId = organizationId, + IncludeGroups = includeGroups, + IncludeCollections = includeCollections + }, + commandType: CommandType.StoredProcedure); + + // Read the user details (first result set) + var users = (await results.ReadAsync()).ToList(); + + // Read group associations (second result set, if requested) + Dictionary>? userGroupMap = null; + if (includeGroups) + { + var groupUsers = await results.ReadAsync(); + userGroupMap = groupUsers + .GroupBy(gu => gu.OrganizationUserId) + .ToDictionary(g => g.Key, g => g.Select(gu => gu.GroupId).ToList()); + } + + // Read collection associations (third result set, if requested) + Dictionary>? userCollectionMap = null; + if (includeCollections) + { + var collectionUsers = await results.ReadAsync(); + userCollectionMap = collectionUsers + .GroupBy(cu => cu.OrganizationUserId) + .ToDictionary(g => g.Key, g => g.Select(cu => new CollectionAccessSelection + { + Id = cu.CollectionId, + ReadOnly = cu.ReadOnly, + HidePasswords = cu.HidePasswords, + Manage = cu.Manage + }).ToList()); + } + + // Map the associations to users + foreach (var user in users) + { + if (userGroupMap != null) + { + user.Groups = userGroupMap.GetValueOrDefault(user.Id, new List()); + } + + if (userCollectionMap != null) + { + user.Collections = userCollectionMap.GetValueOrDefault(user.Id, new List()); + } + } + + return users; + } + } + public async Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null) { @@ -558,6 +620,19 @@ public class OrganizationUserRepository : Repository, IO } } + public async Task> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task RevokeManyByIdAsync(IEnumerable organizationUserIds) { await using var connection = new SqlConnection(ConnectionString); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index bb392a2e60..a6bbf8e6e0 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -404,6 +404,56 @@ public class OrganizationUserRepository : Repository> GetManyDetailsByOrganizationAsync_vNext( + Guid organizationId, bool includeGroups, bool includeCollections) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var query = from ou in dbContext.OrganizationUsers + where ou.OrganizationId == organizationId + select new OrganizationUserUserDetails + { + Id = ou.Id, + UserId = ou.UserId, + OrganizationId = ou.OrganizationId, + Name = ou.User.Name, + Email = ou.User.Email ?? ou.Email, + AvatarColor = ou.User.AvatarColor, + TwoFactorProviders = ou.User.TwoFactorProviders, + Premium = ou.User.Premium, + Status = ou.Status, + Type = ou.Type, + ExternalId = ou.ExternalId, + SsoExternalId = ou.User.SsoUsers + .Where(su => su.OrganizationId == ou.OrganizationId) + .Select(su => su.ExternalId) + .FirstOrDefault(), + Permissions = ou.Permissions, + ResetPasswordKey = ou.ResetPasswordKey, + UsesKeyConnector = ou.User != null && ou.User.UsesKeyConnector, + AccessSecretsManager = ou.AccessSecretsManager, + HasMasterPassword = ou.User != null && !string.IsNullOrWhiteSpace(ou.User.MasterPassword), + + // Project directly from navigation properties with conditional loading + Groups = includeGroups + ? ou.GroupUsers.Select(gu => gu.GroupId).ToList() + : new List(), + + Collections = includeCollections + ? ou.CollectionUsers.Select(cu => new CollectionAccessSelection + { + Id = cu.CollectionId, + ReadOnly = cu.ReadOnly, + HidePasswords = cu.HidePasswords, + Manage = cu.Manage + }).ToList() + : new List() + }; + + return await query.ToListAsync(); + } + public async Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null) { @@ -732,6 +782,12 @@ public class OrganizationUserRepository : Repository> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId) + { + // No EF optimization is required for this query + return await GetManyByOrganizationWithClaimedDomainsAsync(organizationId); + } + public async Task RevokeManyByIdAsync(IEnumerable organizationUserIds) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql new file mode 100644 index 0000000000..6bf32089c2 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql @@ -0,0 +1,31 @@ +CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2] + @OrganizationId UNIQUEIDENTIFIER, + @IncludeGroups BIT = 0, + @IncludeCollections BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + -- Result Set 1: User Details (always returned) + SELECT * + FROM [dbo].[OrganizationUserUserDetailsView] + WHERE OrganizationId = @OrganizationId + + -- Result Set 2: Group associations (if requested) + IF @IncludeGroups = 1 + BEGIN + SELECT gu.* + FROM [dbo].[GroupUser] gu + INNER JOIN [dbo].[OrganizationUser] ou ON gu.OrganizationUserId = ou.Id + WHERE ou.OrganizationId = @OrganizationId + END + + -- Result Set 3: Collection associations (if requested) + IF @IncludeCollections = 1 + BEGIN + SELECT cu.* + FROM [dbo].[CollectionUser] cu + INNER JOIN [dbo].[OrganizationUser] ou ON cu.OrganizationUserId = ou.Id + WHERE ou.OrganizationId = @OrganizationId + END +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.sql new file mode 100644 index 0000000000..64f3d81e08 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + WITH OrgUsers AS ( + SELECT * + FROM [dbo].[OrganizationUserView] + WHERE [OrganizationId] = @OrganizationId + ), + UserDomains AS ( + SELECT U.[Id], U.[EmailDomain] + FROM [dbo].[UserEmailDomainView] U + WHERE EXISTS ( + SELECT 1 + FROM [dbo].[OrganizationDomainView] OD + WHERE OD.[OrganizationId] = @OrganizationId + AND OD.[VerifiedDate] IS NOT NULL + AND OD.[DomainName] = U.[EmailDomain] + ) + ) + SELECT OU.* + FROM OrgUsers OU + JOIN UserDomains UD ON OU.[UserId] = UD.[Id] + OPTION (RECOMPILE); +END diff --git a/src/Sql/dbo/Tables/OrganizationDomain.sql b/src/Sql/dbo/Tables/OrganizationDomain.sql index 615dcc1557..582029acfe 100644 --- a/src/Sql/dbo/Tables/OrganizationDomain.sql +++ b/src/Sql/dbo/Tables/OrganizationDomain.sql @@ -25,5 +25,11 @@ GO CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_DomainNameVerifiedDateOrganizationId] ON [dbo].[OrganizationDomain] ([DomainName],[VerifiedDate]) - INCLUDE ([OrganizationId]) + INCLUDE ([OrganizationId]); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_OrganizationId_VerifiedDate] + ON [dbo].[OrganizationDomain] ([OrganizationId], [VerifiedDate]) + INCLUDE ([DomainName]) + WHERE [VerifiedDate] IS NOT NULL; GO diff --git a/src/Sql/dbo/Tables/OrganizationUser.sql b/src/Sql/dbo/Tables/OrganizationUser.sql index 331e85fe63..513a5f6696 100644 --- a/src/Sql/dbo/Tables/OrganizationUser.sql +++ b/src/Sql/dbo/Tables/OrganizationUser.sql @@ -27,3 +27,10 @@ GO CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId] ON [dbo].[OrganizationUser]([OrganizationId] ASC); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId] + ON [dbo].[OrganizationUser] ([OrganizationId], [UserId]) + INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], + [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]); +GO diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index 188dd4ea3c..239ee67f11 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -54,3 +54,7 @@ GO CREATE NONCLUSTERED INDEX [IX_User_Premium_PremiumExpirationDate_RenewalReminderDate] ON [dbo].[User]([Premium] ASC, [PremiumExpirationDate] ASC, [RenewalReminderDate] ASC); +GO +CREATE NONCLUSTERED INDEX [IX_User_Id_EmailDomain] + ON [dbo].[User]([Id] ASC, [Email] ASC); + diff --git a/src/Sql/dbo/Views/UserEmailDomainView.sql b/src/Sql/dbo/Views/UserEmailDomainView.sql new file mode 100644 index 0000000000..84930a41f1 --- /dev/null +++ b/src/Sql/dbo/Views/UserEmailDomainView.sql @@ -0,0 +1,10 @@ +CREATE VIEW [dbo].[UserEmailDomainView] +AS +SELECT + Id, + Email, + SUBSTRING(Email, CHARINDEX('@', Email) + 1, LEN(Email)) AS EmailDomain +FROM dbo.[User] +WHERE Email IS NOT NULL + AND CHARINDEX('@', Email) > 0 +GO diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 6919ce7bce..8eec878794 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -142,18 +142,24 @@ public class OrganizationUserRepositoryTests var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user2.Id, - Status = OrganizationUserStatusType.Confirmed, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, ResetPasswordKey = "resetpasswordkey2", + AccessSecretsManager = true }); var recoveryDetails = await organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync( @@ -292,10 +298,13 @@ public class OrganizationUserRepositoryTests var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); var responseModel = await organizationUserRepository.GetManyDetailsByUserAsync(user1.Id); @@ -435,27 +444,35 @@ public class OrganizationUserRepositoryTests var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, ResetPasswordKey = "resetpasswordkey1", AccessSecretsManager = false }); await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user2.Id, Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); await organizationUserRepository.CreateAsync(new OrganizationUser { + Id = CoreHelpers.GenerateComb(), OrganizationId = organization.Id, UserId = user3.Id, Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); @@ -724,4 +741,410 @@ public class OrganizationUserRepositoryTests Assert.Equal(collection3.Id, orgUser3.Collections.First().Id); Assert.Equal(group3.Id, group3Database.First()); } + + [DatabaseTheory, DatabaseData] + public async Task GetManyDetailsByOrganizationAsync_vNext_WithoutGroupsAndCollections_ReturnsBasicUserDetails( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + var id = Guid.NewGuid(); + + var user1 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 1", + Email = $"test1+{id}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var user2 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 2", + Email = $"test2+{id}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.Argon2id, + KdfIterations = 4, + KdfMemory = 5, + KdfParallelism = 6 + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Id = CoreHelpers.GenerateComb(), + Name = $"Test Org {id}", + BillingEmail = user1.Email, + Plan = "Test", + PrivateKey = "privatekey", + PublicKey = "publickey", + UseGroups = true, + Enabled = true, + UsePasswordManager = true + }); + + var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false + }); + + var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user2.Id, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, + ResetPasswordKey = "resetpasswordkey2", + AccessSecretsManager = true + }); + + var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeCollections: false); + + Assert.NotNull(responseModel); + Assert.Equal(2, responseModel.Count); + + var user1Result = responseModel.FirstOrDefault(u => u.Id == orgUser1.Id); + Assert.NotNull(user1Result); + Assert.Equal(user1.Name, user1Result.Name); + Assert.Equal(user1.Email, user1Result.Email); + Assert.Equal(orgUser1.Status, user1Result.Status); + Assert.Equal(orgUser1.Type, user1Result.Type); + Assert.Equal(organization.Id, user1Result.OrganizationId); + Assert.Equal(user1.Id, user1Result.UserId); + Assert.Empty(user1Result.Groups); + Assert.Empty(user1Result.Collections); + + var user2Result = responseModel.FirstOrDefault(u => u.Id == orgUser2.Id); + Assert.NotNull(user2Result); + Assert.Equal(user2.Name, user2Result.Name); + Assert.Equal(user2.Email, user2Result.Email); + Assert.Equal(orgUser2.Status, user2Result.Status); + Assert.Equal(orgUser2.Type, user2Result.Type); + Assert.Equal(organization.Id, user2Result.OrganizationId); + Assert.Equal(user2.Id, user2Result.UserId); + Assert.Empty(user2Result.Groups); + Assert.Empty(user2Result.Collections); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyDetailsByOrganizationAsync_vNext_WithGroupsAndCollections_ReturnsUserDetailsWithBoth( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + var id = Guid.NewGuid(); + var requestTime = DateTime.UtcNow; + + var user1 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 1", + Email = $"test1+{id}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Id = CoreHelpers.GenerateComb(), + Name = $"Test Org {id}", + BillingEmail = user1.Email, + Plan = "Test", + PrivateKey = "privatekey", + PublicKey = "publickey", + UseGroups = true, + Enabled = true + }); + + var group1 = await groupRepository.CreateAsync(new Group + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "Test Group 1", + ExternalId = "external-group-1" + }); + + var group2 = await groupRepository.CreateAsync(new Group + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "Test Group 2", + ExternalId = "external-group-2" + }); + + var collection1 = await collectionRepository.CreateAsync(new Collection + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "Test Collection 1", + ExternalId = "external-collection-1", + CreationDate = requestTime, + RevisionDate = requestTime + }); + + var collection2 = await collectionRepository.CreateAsync(new Collection + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "Test Collection 2", + ExternalId = "external-collection-2", + CreationDate = requestTime, + RevisionDate = requestTime + }); + + // Create organization user with both groups and collections using CreateManyAsync + var createOrgUserWithCollections = new List + { + new() + { + OrganizationUser = new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + AccessSecretsManager = false + }, + Collections = + [ + new CollectionAccessSelection + { + Id = collection1.Id, + ReadOnly = true, + HidePasswords = false, + Manage = false + }, + new CollectionAccessSelection + { + Id = collection2.Id, + ReadOnly = false, + HidePasswords = true, + Manage = true + } + ], + Groups = [group1.Id, group2.Id] + } + }; + + await organizationUserRepository.CreateManyAsync(createOrgUserWithCollections); + + var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeCollections: true); + + Assert.NotNull(responseModel); + Assert.Single(responseModel); + + var user1Result = responseModel.First(); + + Assert.Equal(user1.Name, user1Result.Name); + Assert.Equal(user1.Email, user1Result.Email); + Assert.Equal(organization.Id, user1Result.OrganizationId); + Assert.Equal(user1.Id, user1Result.UserId); + + Assert.NotNull(user1Result.Groups); + Assert.Equal(2, user1Result.Groups.Count()); + Assert.Contains(group1.Id, user1Result.Groups); + Assert.Contains(group2.Id, user1Result.Groups); + + Assert.NotNull(user1Result.Collections); + Assert.Equal(2, user1Result.Collections.Count()); + Assert.Contains(user1Result.Collections, c => c.Id == collection1.Id); + Assert.Contains(user1Result.Collections, c => c.Id == collection2.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByOrganizationWithClaimedDomainsAsync_vNext_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + var requestTime = DateTime.UtcNow; + + var user1 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 1", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + CreationDate = requestTime, + RevisionDate = requestTime, + AccountRevisionDate = requestTime + }); + + var user2 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 2", + Email = $"test+{id}@x-{domainName}", // Different domain + ApiKey = "TEST", + SecurityStamp = "stamp", + CreationDate = requestTime, + RevisionDate = requestTime, + AccountRevisionDate = requestTime + }); + + var user3 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 3", + Email = $"test+{id}@{domainName}.example.com", // Different domain + ApiKey = "TEST", + SecurityStamp = "stamp", + CreationDate = requestTime, + RevisionDate = requestTime, + AccountRevisionDate = requestTime + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Id = CoreHelpers.GenerateComb(), + Name = $"Test Org {id}", + BillingEmail = user1.Email, + Plan = "Test", + Enabled = true, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + var organizationDomain = new OrganizationDomain + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345", + CreationDate = requestTime + }; + organizationDomain.SetNextRunDate(12); + organizationDomain.SetVerifiedDate(); + organizationDomain.SetJobRunCount(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user2.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user3.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organization.Id); + + Assert.NotNull(responseModel); + Assert.Single(responseModel); + Assert.Equal(orgUser1.Id, responseModel.Single().Id); + Assert.Equal(user1.Id, responseModel.Single().UserId); + Assert.Equal(organization.Id, responseModel.Single().OrganizationId); + } + + [DatabaseTheory, DatabaseData] + public async Task GetManyByOrganizationWithClaimedDomainsAsync_vNext_WithNoVerifiedDomain_ReturnsEmpty( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + var requestTime = DateTime.UtcNow; + + var user1 = await userRepository.CreateAsync(new User + { + Id = CoreHelpers.GenerateComb(), + Name = "Test User 1", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + CreationDate = requestTime, + RevisionDate = requestTime, + AccountRevisionDate = requestTime + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Id = CoreHelpers.GenerateComb(), + Name = $"Test Org {id}", + BillingEmail = user1.Email, + Plan = "Test", + Enabled = true, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + // Create domain but do NOT verify it + var organizationDomain = new OrganizationDomain + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345", + CreationDate = requestTime + }; + organizationDomain.SetNextRunDate(12); + // Note: NOT calling SetVerifiedDate() + await organizationDomainRepository.CreateAsync(organizationDomain); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + CreationDate = requestTime, + RevisionDate = requestTime + }); + + var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organization.Id); + + Assert.NotNull(responseModel); + Assert.Empty(responseModel); + } } diff --git a/util/Migrator/DbScripts/2025-07-22_00_OrgUsersQueryOptimizations.sql b/util/Migrator/DbScripts/2025-07-22_00_OrgUsersQueryOptimizations.sql new file mode 100644 index 0000000000..7a1ba68276 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-22_00_OrgUsersQueryOptimizations.sql @@ -0,0 +1,98 @@ +CREATE OR ALTER VIEW [dbo].[UserEmailDomainView] +AS +SELECT + Id, + Email, + SUBSTRING(Email, CHARINDEX('@', Email) + 1, LEN(Email)) AS EmailDomain +FROM dbo.[User] +WHERE Email IS NOT NULL + AND CHARINDEX('@', Email) > 0 +GO + +-- Index on OrganizationUser for efficient filtering +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationUser_OrganizationId_UserId') + BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId] + ON [dbo].[OrganizationUser] ([OrganizationId], [UserId]) + INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], + [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]) + END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_User_Id_EmailDomain') + BEGIN + CREATE NONCLUSTERED INDEX [IX_User_Id_EmailDomain] + ON [dbo].[User] ([Id], [Email]) + END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationDomain_OrganizationId_VerifiedDate') + BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationDomain_OrganizationId_VerifiedDate] + ON [dbo].[OrganizationDomain] ([OrganizationId], [VerifiedDate]) + INCLUDE ([DomainName]) + WHERE [VerifiedDate] IS NOT NULL + END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2] + @OrganizationId UNIQUEIDENTIFIER, + @IncludeGroups BIT = 0, + @IncludeCollections BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + -- Result Set 1: User Details (always returned) + SELECT * + FROM [dbo].[OrganizationUserUserDetailsView] + WHERE OrganizationId = @OrganizationId + + -- Result Set 2: Group associations (if requested) + IF @IncludeGroups = 1 + BEGIN + SELECT gu.* + FROM [dbo].[GroupUser] gu + INNER JOIN [dbo].[OrganizationUser] ou ON gu.OrganizationUserId = ou.Id + WHERE ou.OrganizationId = @OrganizationId + END + + -- Result Set 3: Collection associations (if requested) + IF @IncludeCollections = 1 + BEGIN + SELECT cu.* + FROM [dbo].[CollectionUser] cu + INNER JOIN [dbo].[OrganizationUser] ou ON cu.OrganizationUserId = ou.Id + WHERE ou.OrganizationId = @OrganizationId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON; + + WITH OrgUsers AS ( + SELECT * + FROM [dbo].[OrganizationUserView] + WHERE [OrganizationId] = @OrganizationId + ), + UserDomains AS ( + SELECT U.[Id], U.[EmailDomain] + FROM [dbo].[UserEmailDomainView] U + WHERE EXISTS ( + SELECT 1 + FROM [dbo].[OrganizationDomainView] OD + WHERE OD.[OrganizationId] = @OrganizationId + AND OD.[VerifiedDate] IS NOT NULL + AND OD.[DomainName] = U.[EmailDomain] + ) + ) + SELECT OU.* + FROM OrgUsers OU + JOIN UserDomains UD ON OU.[UserId] = UD.[Id] + OPTION (RECOMPILE); +END +GO From 141f8bf8b2977bf5c7f704cc4029f8607da30f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:20:04 +0100 Subject: [PATCH 080/326] [PM-21031] Update Members Get Endpoint Optimization feature flag key to match LaunchDarkly (#6115) --- src/Core/Constants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f040dcc9e8..9573a0ce0a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,7 @@ public static class FeatureFlagKeys public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; - public const string MembersGetEndpointOptimization = "pm-21031-members-get-endpoint-optimization"; + public const string MembersGetEndpointOptimization = "pm-23113-optimize-get-members-endpoint"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; From 829c3ed1d71b65f96fcf8e033f47a461bd4e73e7 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:25:37 -0500 Subject: [PATCH 081/326] [PM-21821] Provider portal takeover states (#6109) * Add feature flag * Disable provider and schedule cancellation when subscription goes unpaid * Run dotnet format * Only set provider subscription cancel_at when subscription is going from paid to unpaid * Update tests --- src/Billing/Billing.csproj | 1 + src/Billing/Services/IStripeFacade.cs | 7 + .../Services/Implementations/StripeFacade.cs | 10 + .../SubscriptionUpdatedHandler.cs | 92 ++++- src/Billing/Startup.cs | 2 + src/Core/Constants.cs | 1 + .../SubscriptionUpdatedHandlerTests.cs | 314 +++++++++++++++++- 7 files changed, 424 insertions(+), 3 deletions(-) diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 116efdb68c..25327b17b7 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -6,6 +6,7 @@ + diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index 6886250a33..37ba51cc61 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -2,6 +2,7 @@ #nullable disable using Stripe; +using Stripe.TestHelpers; namespace Bit.Billing.Services; @@ -98,4 +99,10 @@ public interface IStripeFacade string subscriptionId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task GetTestClock( + string testClockId, + TestClockGetOptions testClockGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default); } diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index 70144d8cd3..726a3e977c 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -2,6 +2,8 @@ #nullable disable using Stripe; +using Stripe.TestHelpers; +using CustomerService = Stripe.CustomerService; namespace Bit.Billing.Services.Implementations; @@ -14,6 +16,7 @@ public class StripeFacade : IStripeFacade private readonly PaymentMethodService _paymentMethodService = new(); private readonly SubscriptionService _subscriptionService = new(); private readonly DiscountService _discountService = new(); + private readonly TestClockService _testClockService = new(); public async Task GetCharge( string chargeId, @@ -119,4 +122,11 @@ public class StripeFacade : IStripeFacade RequestOptions requestOptions = null, CancellationToken cancellationToken = default) => await _discountService.DeleteSubscriptionDiscountAsync(subscriptionId, requestOptions, cancellationToken); + + public Task GetTestClock( + string testClockId, + TestClockGetOptions testClockGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) => + _testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken); } diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index fe5021c827..bbc17aa3b2 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,6 +1,9 @@ using Bit.Billing.Constants; using Bit.Billing.Jobs; +using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; @@ -8,6 +11,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Quartz; using Stripe; +using Stripe.TestHelpers; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -26,6 +30,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; + private readonly IFeatureService _featureService; + private readonly IProviderRepository _providerRepository; + private readonly IProviderService _providerService; + private readonly ILogger _logger; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -39,7 +47,11 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler ISchedulerFactory schedulerFactory, IOrganizationEnableCommand organizationEnableCommand, IOrganizationDisableCommand organizationDisableCommand, - IPricingClient pricingClient) + IPricingClient pricingClient, + IFeatureService featureService, + IProviderRepository providerRepository, + IProviderService providerService, + ILogger logger) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -53,6 +65,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _organizationEnableCommand = organizationEnableCommand; _organizationDisableCommand = organizationDisableCommand; _pricingClient = pricingClient; + _featureService = featureService; + _providerRepository = providerRepository; + _providerService = providerService; + _logger = logger; } /// @@ -61,7 +77,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler /// public async Task HandleAsync(Event parsedEvent) { - var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice"]); + var subscription = await _stripeEventService.GetSubscription(parsedEvent, true, ["customer", "discounts", "latest_invoice", "test_clock"]); var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); switch (subscription.Status) @@ -77,6 +93,11 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } break; } + case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired when providerId.HasValue: + { + await HandleUnpaidProviderSubscriptionAsync(providerId.Value, parsedEvent, subscription); + break; + } case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired: { if (!userId.HasValue) @@ -238,4 +259,71 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler await scheduler.ScheduleJob(job, trigger); } + + private async Task HandleUnpaidProviderSubscriptionAsync( + Guid providerId, + Event parsedEvent, + Subscription subscription) + { + var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + + if (!providerPortalTakeover) + { + return; + } + + var provider = await _providerRepository.GetByIdAsync(providerId); + if (provider == null) + { + return; + } + + try + { + provider.Enabled = false; + await _providerService.UpdateAsync(provider); + + if (parsedEvent.Data.PreviousAttributes != null) + { + if (parsedEvent.Data.PreviousAttributes.ToObject() as Subscription is + { + Status: + StripeSubscriptionStatus.Trialing or + StripeSubscriptionStatus.Active or + StripeSubscriptionStatus.PastDue + } && subscription is + { + Status: StripeSubscriptionStatus.Unpaid, + LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create" + }) + { + if (subscription.TestClock != null) + { + await WaitForTestClockToAdvanceAsync(subscription.TestClock); + } + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + await _stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) }); + } + } + } + catch (Exception exception) + { + _logger.LogError(exception, "An error occurred while trying to disable and schedule subscription cancellation for provider ({ProviderID})", providerId); + } + } + + private async Task WaitForTestClockToAdvanceAsync(TestClock testClock) + { + while (testClock.Status != "ready") + { + await Task.Delay(TimeSpan.FromSeconds(2)); + testClock = await _stripeFacade.GetTestClock(testClock.Id); + if (testClock.Status == "internal_failure") + { + throw new Exception("Stripe Test Clock encountered an internal failure"); + } + } + } } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 24b5372ba1..cfbc90c36e 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Net.Http.Headers; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; +using Bit.Commercial.Core.Utilities; using Bit.Core.Billing.Extensions; using Bit.Core.Context; using Bit.Core.SecretsManager.Repositories; @@ -83,6 +84,7 @@ public class Startup services.AddDefaultServices(globalSettings); services.AddDistributedCache(globalSettings); services.AddBillingOperations(); + services.AddCommercialCoreServices(); services.TryAddSingleton(); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9573a0ce0a..08191ff356 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -159,6 +159,7 @@ public static class FeatureFlagKeys public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; + public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index 9c58bbdbf7..ce4ee608cc 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -1,8 +1,12 @@ using Bit.Billing.Constants; using Bit.Billing.Services; using Bit.Billing.Services.Implementations; +using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; @@ -10,10 +14,12 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using NSubstitute; using Quartz; using Stripe; +using Stripe.TestHelpers; using Xunit; using Event = Stripe.Event; @@ -33,6 +39,10 @@ public class SubscriptionUpdatedHandlerTests private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; + private readonly IFeatureService _featureService; + private readonly IProviderRepository _providerRepository; + private readonly IProviderService _providerService; + private readonly ILogger _logger; private readonly IScheduler _scheduler; private readonly SubscriptionUpdatedHandler _sut; @@ -50,6 +60,10 @@ public class SubscriptionUpdatedHandlerTests _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); _pricingClient = Substitute.For(); + _featureService = Substitute.For(); + _providerRepository = Substitute.For(); + _providerService = Substitute.For(); + _logger = Substitute.For>(); _scheduler = Substitute.For(); _schedulerFactory.GetScheduler().Returns(_scheduler); @@ -66,7 +80,11 @@ public class SubscriptionUpdatedHandlerTests _schedulerFactory, _organizationEnableCommand, _organizationDisableCommand, - _pricingClient); + _pricingClient, + _featureService, + _providerRepository, + _providerService, + _logger); } [Fact] @@ -104,6 +122,300 @@ public class SubscriptionUpdatedHandlerTests Arg.Is(t => t.Key.Name == $"cancel-trigger-{subscriptionId}")); } + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation() + { + // Arrange + var providerId = Guid.NewGuid(); + const string subscriptionId = "sub_123"; + var frozenTime = DateTime.UtcNow; + + var testClock = new TestClock + { + Id = "clock_123", + Status = "ready", + FrozenTime = frozenTime + }; + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }, + TestClock = testClock + }; + + var provider = new Provider + { + Id = providerId, + Name = "Test Provider", + Enabled = true + }; + + var parsedEvent = new Event + { + Data = new EventData + { + PreviousAttributes = JObject.FromObject(new + { + status = "active" + }) + } + }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + _stripeFacade.GetTestClock(testClock.Id) + .Returns(testClock); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + Assert.False(provider.Enabled); + await _providerService.Received(1).UpdateAsync(provider); + await _stripeFacade.Received(1).UpdateSubscription(subscriptionId, + Arg.Is(o => o.CancelAt == frozenTime.AddDays(7))); + } + + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WithoutValidTransition_DisablesProviderOnly() + { + // Arrange + var providerId = Guid.NewGuid(); + const string subscriptionId = "sub_123"; + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } + }; + + var provider = new Provider + { + Id = providerId, + Name = "Test Provider", + Enabled = true + }; + + var parsedEvent = new Event + { + Data = new EventData + { + PreviousAttributes = JObject.FromObject(new + { + status = "unpaid" // No valid transition + }) + } + }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + Assert.False(provider.Enabled); + await _providerService.Received(1).UpdateAsync(provider); + await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WithNoPreviousAttributes_DisablesProviderOnly() + { + // Arrange + var providerId = Guid.NewGuid(); + const string subscriptionId = "sub_123"; + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } + }; + + var provider = new Provider + { + Id = providerId, + Name = "Test Provider", + Enabled = true + }; + + var parsedEvent = new Event + { + Data = new EventData + { + PreviousAttributes = null + } + }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + Assert.False(provider.Enabled); + await _providerService.Received(1).UpdateAsync(provider); + await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WithIncompleteExpiredStatus_DisablesProvider() + { + // Arrange + var providerId = Guid.NewGuid(); + var subscriptionId = "sub_123"; + var currentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.IncompleteExpired, + CurrentPeriodEnd = currentPeriodEnd, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "renewal" } + }; + + var provider = new Provider + { + Id = providerId, + Name = "Test Provider", + Enabled = true + }; + + var parsedEvent = new Event { Data = new EventData() }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + Assert.False(provider.Enabled); + await _providerService.Received(1).UpdateAsync(provider); + await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WhenFeatureFlagDisabled_DoesNothing() + { + // Arrange + var providerId = Guid.NewGuid(); + var subscriptionId = "sub_123"; + var currentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + CurrentPeriodEnd = currentPeriodEnd, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } + }; + + var parsedEvent = new Event { Data = new EventData() }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(false); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _providerRepository.DidNotReceive().GetByIdAsync(Arg.Any()); + await _providerService.DidNotReceive().UpdateAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_UnpaidProviderSubscription_WhenProviderNotFound_DoesNothing() + { + // Arrange + var providerId = Guid.NewGuid(); + var subscriptionId = "sub_123"; + var currentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var subscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + CurrentPeriodEnd = currentPeriodEnd, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } + }; + + var parsedEvent = new Event { Data = new EventData() }; + + _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(subscription); + + _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + _providerRepository.GetByIdAsync(providerId) + .Returns((Provider)null); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _providerService.DidNotReceive().UpdateAsync(Arg.Any()); + await _stripeFacade.DidNotReceive().UpdateSubscription(Arg.Any(), Arg.Any()); + } + [Fact] public async Task HandleAsync_UnpaidUserSubscription_DisablesPremiumAndCancelsSubscription() { From 988b9946240739df2df87ab533678e3a5f2190b1 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:24:59 -0400 Subject: [PATCH 082/326] [PM-17562] Add GET endpoints for event integrations (#6104) * [PM-17562] Add GET endpoints for event integrations * Default to null for Service * Respond to PR Feedback --- ...ationIntegrationConfigurationController.cs | 21 +++ .../OrganizationIntegrationController.cs | 14 ++ .../OrganizationIntegrationResponseModel.cs | 2 + .../Data/EventIntegrations/HecIntegration.cs | 2 +- ...ationIntegrationConfigurationRepository.cs | 2 + .../IOrganizationIntegrationRepository.cs | 1 + ...ationIntegrationConfigurationRepository.cs | 16 +++ .../OrganizationIntegrationRepository.cs | 18 ++- ...ationIntegrationConfigurationRepository.cs | 14 ++ .../OrganizationIntegrationRepository.cs | 19 ++- ...eadManyByOrganizationIntegrationIdQuery.cs | 33 +++++ ...ntegrationReadManyByOrganizationIdQuery.cs | 30 +++++ ...on_ReadManyByOrganizationIntegrationId.sql | 13 ++ ...onIntegration_ReadManyByOrganizationId.sql | 13 ++ .../OrganizationIntegrationControllerTests.cs | 54 ++++++++ ...ntegrationsConfigurationControllerTests.cs | 125 ++++++++++++++++++ ...onIntegrationAndConfigurationByIdProcs.sql | 29 ++++ 17 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadManyByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-07-18_00_AddReadManyOrganizationIntegrationAndConfigurationByIdProcs.sql diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs index 848098ef00..319fbbe707 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs @@ -18,6 +18,27 @@ public class OrganizationIntegrationConfigurationController( IOrganizationIntegrationRepository integrationRepository, IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller { + [HttpGet("")] + public async Task> GetAsync( + Guid organizationId, + Guid integrationId) + { + if (!await HasPermission(organizationId)) + { + throw new NotFoundException(); + } + var integration = await integrationRepository.GetByIdAsync(integrationId); + if (integration == null || integration.OrganizationId != organizationId) + { + throw new NotFoundException(); + } + + var configurations = await integrationConfigurationRepository.GetManyByIntegrationAsync(integrationId); + return configurations + .Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration)) + .ToList(); + } + [HttpPost("")] public async Task CreateAsync( Guid organizationId, diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs index 3b52e7a8da..7052350c9a 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs @@ -19,6 +19,20 @@ public class OrganizationIntegrationController( ICurrentContext currentContext, IOrganizationIntegrationRepository integrationRepository) : Controller { + [HttpGet("")] + public async Task> GetAsync(Guid organizationId) + { + if (!await HasPermission(organizationId)) + { + throw new NotFoundException(); + } + + var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId); + return integrations + .Select(integration => new OrganizationIntegrationResponseModel(integration)) + .ToList(); + } + [HttpPost("")] public async Task CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model) { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs index cc6e778528..f062ff46a2 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs @@ -15,8 +15,10 @@ public class OrganizationIntegrationResponseModel : ResponseModel Id = organizationIntegration.Id; Type = organizationIntegration.Type; + Configuration = organizationIntegration.Configuration; } public Guid Id { get; set; } public IntegrationType Type { get; set; } + public string? Configuration { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs index 472ca70c0c..eff9f8e1be 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record HecIntegration(Uri Uri, string Scheme, string Token); +public record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null); diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs index 53159c98e7..0a774cf395 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -12,4 +12,6 @@ public interface IOrganizationIntegrationConfigurationRepository : IRepository> GetAllConfigurationDetailsAsync(); + + Task> GetManyByIntegrationAsync(Guid organizationIntegrationId); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs index cd7700c310..434c8ddee3 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs @@ -4,4 +4,5 @@ namespace Bit.Core.Repositories; public interface IOrganizationIntegrationRepository : IRepository { + Task> GetManyByOrganizationAsync(Guid organizationId); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs index 5a7e1ce152..005e93c6aa 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -52,4 +52,20 @@ public class OrganizationIntegrationConfigurationRepository : Repository> GetManyByIntegrationAsync(Guid organizationIntegrationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]", + new + { + OrganizationIntegrationId = organizationIntegrationId + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs index 99f0e35378..ece9697a31 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -1,6 +1,9 @@ -using Bit.Core.AdminConsole.Entities; +using System.Data; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Repositories; using Bit.Core.Settings; +using Dapper; +using Microsoft.Data.SqlClient; namespace Bit.Infrastructure.Dapper.Repositories; @@ -13,4 +16,17 @@ public class OrganizationIntegrationRepository : Repository> GetManyByOrganizationAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationIntegration_ReadManyByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs index 1e1dcd3ba4..fc391b958c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -3,6 +3,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Microsoft.EntityFrameworkCore; @@ -40,4 +41,17 @@ public class OrganizationIntegrationConfigurationRepository : Repository> GetManyByIntegrationAsync( + Guid organizationIntegrationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery( + organizationIntegrationId + ); + return await query.Run(dbContext).ToListAsync(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs index 816ad3b25f..5670b2ae9b 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -1,14 +1,29 @@ using AutoMapper; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; -public class OrganizationIntegrationRepository : Repository, IOrganizationIntegrationRepository +public class OrganizationIntegrationRepository : + Repository, + IOrganizationIntegrationRepository { public OrganizationIntegrationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationIntegrations) - { } + { + } + + public async Task> GetManyByOrganizationAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new OrganizationIntegrationReadManyByOrganizationIdQuery(organizationId); + return await query.Run(dbContext).ToListAsync(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs new file mode 100644 index 0000000000..3ed3a48723 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery.cs @@ -0,0 +1,33 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; + +public class OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery : IQuery +{ + private readonly Guid _organizationIntegrationId; + + public OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery(Guid organizationIntegrationId) + { + _organizationIntegrationId = organizationIntegrationId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from oic in dbContext.OrganizationIntegrationConfigurations + where oic.OrganizationIntegrationId == _organizationIntegrationId + select new OrganizationIntegrationConfiguration() + { + Id = oic.Id, + OrganizationIntegrationId = oic.OrganizationIntegrationId, + Configuration = oic.Configuration, + EventType = oic.EventType, + Filters = oic.Filters, + Template = oic.Template, + RevisionDate = oic.RevisionDate + }; + return query; + } + +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs new file mode 100644 index 0000000000..df87ad0bc1 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationReadManyByOrganizationIdQuery.cs @@ -0,0 +1,30 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries; + +public class OrganizationIntegrationReadManyByOrganizationIdQuery : IQuery +{ + private readonly Guid _organizationId; + + public OrganizationIntegrationReadManyByOrganizationIdQuery(Guid organizationId) + { + _organizationId = organizationId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from oi in dbContext.OrganizationIntegrations + where oi.OrganizationId == _organizationId + select new OrganizationIntegration() + { + Id = oi.Id, + OrganizationId = oi.OrganizationId, + Type = oi.Type, + Configuration = oi.Configuration, + }; + return query; + } + +} diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId.sql new file mode 100644 index 0000000000..b187ff1d5f --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId] + @OrganizationIntegrationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationIntegrationConfigurationView] +WHERE + [OrganizationIntegrationId] = @OrganizationIntegrationId +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadManyByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadManyByOrganizationId.sql new file mode 100644 index 0000000000..939cfc0288 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationIntegration_ReadManyByOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadManyByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationIntegrationView] +WHERE + [OrganizationId] = @OrganizationId +END diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs index fbb3ecbfe0..1dd0e86f39 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationControllerTests.cs @@ -25,6 +25,60 @@ public class OrganizationIntegrationControllerTests Type = IntegrationType.Webhook }; + [Theory, BitAutoData] + public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound( + SutProvider sutProvider, + Guid organizationId) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAsync(organizationId)); + } + + [Theory, BitAutoData] + public async Task GetAsync_IntegrationsExist_ReturnsIntegrations( + SutProvider sutProvider, + Guid organizationId, + List integrations) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId) + .Returns(integrations); + + var result = await sutProvider.Sut.GetAsync(organizationId); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationAsync(organizationId); + + Assert.Equal(integrations.Count, result.Count); + Assert.All(result, r => Assert.IsType(r)); + } + + [Theory, BitAutoData] + public async Task GetAsync_NoIntegrations_ReturnsEmptyList( + SutProvider sutProvider, + Guid organizationId) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetManyByOrganizationAsync(organizationId) + .Returns([]); + + var result = await sutProvider.Sut.GetAsync(organizationId); + + Assert.Empty(result); + } + [Theory, BitAutoData] public async Task CreateAsync_Webhook_AllParamsProvided_Succeeds( SutProvider sutProvider, diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index e2ee854793..4ccfa70308 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -141,6 +141,131 @@ public class OrganizationIntegrationsConfigurationControllerTests await Assert.ThrowsAsync(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty)); } + [Theory, BitAutoData] + public async Task GetAsync_ConfigurationsExist_Succeeds( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration, + List organizationIntegrationConfigurations) + { + organizationIntegration.OrganizationId = organizationId; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .GetManyByIntegrationAsync(Arg.Any()) + .Returns(organizationIntegrationConfigurations); + + var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id); + Assert.NotNull(result); + Assert.Equal(organizationIntegrationConfigurations.Count, result.Count); + Assert.All(result, r => Assert.IsType(r)); + + await sutProvider.GetDependency().Received(1) + .GetByIdAsync(organizationIntegration.Id); + await sutProvider.GetDependency().Received(1) + .GetManyByIntegrationAsync(organizationIntegration.Id); + } + + [Theory, BitAutoData] + public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration) + { + organizationIntegration.OrganizationId = organizationId; + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .GetManyByIntegrationAsync(Arg.Any()) + .Returns([]); + + var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id); + Assert.NotNull(result); + Assert.Empty(result); + + await sutProvider.GetDependency().Received(1) + .GetByIdAsync(organizationIntegration.Id); + await sutProvider.GetDependency().Received(1) + .GetManyByIntegrationAsync(organizationIntegration.Id); + } + + // [Theory, BitAutoData] + // public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound( + // SutProvider sutProvider, + // Guid organizationId, + // OrganizationIntegration organizationIntegration) + // { + // organizationIntegration.OrganizationId = organizationId; + // sutProvider.Sut.Url = Substitute.For(); + // sutProvider.GetDependency() + // .OrganizationOwner(organizationId) + // .Returns(true); + // sutProvider.GetDependency() + // .GetByIdAsync(Arg.Any()) + // .Returns(organizationIntegration); + // sutProvider.GetDependency() + // .GetByIdAsync(Arg.Any()) + // .ReturnsNull(); + // + // await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty)); + // } + // + [Theory, BitAutoData] + public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound( + SutProvider sutProvider, + Guid organizationId) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .ReturnsNull(); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid())); + } + + [Theory, BitAutoData] + public async Task GetAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id)); + } + + [Theory, BitAutoData] + public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound( + SutProvider sutProvider, + Guid organizationId) + { + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(false); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid())); + } + [Theory, BitAutoData] public async Task PostAsync_AllParamsProvided_Slack_Succeeds( SutProvider sutProvider, diff --git a/util/Migrator/DbScripts/2025-07-18_00_AddReadManyOrganizationIntegrationAndConfigurationByIdProcs.sql b/util/Migrator/DbScripts/2025-07-18_00_AddReadManyOrganizationIntegrationAndConfigurationByIdProcs.sql new file mode 100644 index 0000000000..365b3e5350 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-18_00_AddReadManyOrganizationIntegrationAndConfigurationByIdProcs.sql @@ -0,0 +1,29 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegration_ReadManyByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationIntegrationView] +WHERE + [OrganizationId] = @OrganizationId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId] + @OrganizationIntegrationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationIntegrationConfigurationView] +WHERE + [OrganizationIntegrationId] = @OrganizationIntegrationId +END +GO From 2d1f914eae23d0b35d945746f4a8af4d486e59fb Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:59:23 -0500 Subject: [PATCH 083/326] [PM-24067] Check for unverified bank account in free trial / inactive subscription warning (#6117) * [NO LOGIC] Move query to core * Check for unverified bank account in free trial and inactive subscription warnings * Run dotnet format * fix test * Run dotnet format * Remove errant file --- .../OrganizationBillingController.cs | 10 +- src/Api/Billing/Registrations.cs | 11 -- src/Api/Startup.cs | 2 - .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Models/OrganizationWarnings.cs} | 5 +- .../Queries/GetOrganizationWarningsQuery.cs} | 154 +++++++++++------- .../UpdateOrganizationLicenseCommandTests.cs | 2 +- .../GetCloudOrganizationLicenseQueryTests.cs} | 2 +- .../GetOrganizationWarningsQueryTests.cs} | 110 +++++++++++-- ...elfHostedOrganizationLicenseQueryTests.cs} | 2 +- 10 files changed, 200 insertions(+), 99 deletions(-) delete mode 100644 src/Api/Billing/Registrations.cs rename src/{Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs => Core/Billing/Organizations/Models/OrganizationWarnings.cs} (90%) rename src/{Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs => Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs} (51%) rename test/Core.Test/Billing/{OrganizationFeatures/OrganizationLicenses => Organizations/Commands}/UpdateOrganizationLicenseCommandTests.cs (98%) rename test/Core.Test/Billing/{OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs => Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs} (98%) rename test/{Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs => Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs} (70%) rename test/Core.Test/Billing/{OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs => Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs} (98%) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index b9db8d81f9..4915e5ef8e 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -3,10 +3,10 @@ using System.Diagnostics; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Api.Billing.Queries.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; @@ -28,7 +28,7 @@ public class OrganizationBillingController( ICurrentContext currentContext, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, - IOrganizationWarningsQuery organizationWarningsQuery, + IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IPaymentService paymentService, IPricingClient pricingClient, ISubscriberService subscriberService, @@ -363,7 +363,7 @@ public class OrganizationBillingController( public async Task GetWarningsAsync([FromRoute] Guid organizationId) { /* - * We'll keep these available at the User level, because we're hiding any pertinent information and + * We'll keep these available at the User level because we're hiding any pertinent information, and * we want to throw as few errors as possible since these are not core features. */ if (!await currentContext.OrganizationUser(organizationId)) @@ -378,9 +378,9 @@ public class OrganizationBillingController( return Error.NotFound(); } - var response = await organizationWarningsQuery.Run(organization); + var warnings = await getOrganizationWarningsQuery.Run(organization); - return TypedResults.Ok(response); + return TypedResults.Ok(warnings); } diff --git a/src/Api/Billing/Registrations.cs b/src/Api/Billing/Registrations.cs deleted file mode 100644 index cb92098333..0000000000 --- a/src/Api/Billing/Registrations.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.Api.Billing.Queries.Organizations; - -namespace Bit.Api.Billing; - -public static class Registrations -{ - public static void AddBillingQueries(this IServiceCollection services) - { - services.AddTransient(); - } -} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index c2a75c9278..699fa3f804 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -27,7 +27,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; -using Bit.Api.Billing; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Tools.ImportFeatures; @@ -184,7 +183,6 @@ public class Startup services.AddImportServices(); services.AddPhishingDomainServices(globalSettings); - services.AddBillingQueries(); services.AddSendServices(); // Authorization Handlers diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 3fb5526254..39ee3ec1ec 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); + services.AddTransient(); } private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) diff --git a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs similarity index 90% rename from src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs rename to src/Core/Billing/Organizations/Models/OrganizationWarnings.cs index e124bdc318..4507c84083 100644 --- a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs @@ -1,7 +1,6 @@ -#nullable enable -namespace Bit.Api.Billing.Models.Responses.Organizations; +namespace Bit.Core.Billing.Organizations.Models; -public record OrganizationWarningsResponse +public record OrganizationWarnings { public FreeTrialWarning? FreeTrial { get; set; } public InactiveSubscriptionWarning? InactiveSubscription { get; set; } diff --git a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs similarity index 51% rename from src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs rename to src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index 7fbdf3c2b0..a46d7483e7 100644 --- a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -1,42 +1,44 @@ // ReSharper disable InconsistentNaming -#nullable enable - -using Bit.Api.Billing.Models.Responses.Organizations; 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.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; using Stripe; -using static Bit.Core.Billing.Utilities; -using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning; +using FreeTrialWarning = Bit.Core.Billing.Organizations.Models.OrganizationWarnings.FreeTrialWarning; using InactiveSubscriptionWarning = - Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning; + Bit.Core.Billing.Organizations.Models.OrganizationWarnings.InactiveSubscriptionWarning; using ResellerRenewalWarning = - Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning; + Bit.Core.Billing.Organizations.Models.OrganizationWarnings.ResellerRenewalWarning; -namespace Bit.Api.Billing.Queries.Organizations; +namespace Bit.Core.Billing.Organizations.Queries; -public interface IOrganizationWarningsQuery +using static StripeConstants; + +public interface IGetOrganizationWarningsQuery { - Task Run( + Task Run( Organization organization); } -public class OrganizationWarningsQuery( +public class GetOrganizationWarningsQuery( ICurrentContext currentContext, IProviderRepository providerRepository, + ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService) : IOrganizationWarningsQuery + ISubscriberService subscriberService) : IGetOrganizationWarningsQuery { - public async Task Run( + public async Task Run( Organization organization) { - var response = new OrganizationWarningsResponse(); + var response = new OrganizationWarnings(); var subscription = await subscriberService.GetSubscription(organization, @@ -69,7 +71,7 @@ public class OrganizationWarningsQuery( if (subscription is not { - Status: StripeConstants.SubscriptionStatus.Trialing, + Status: SubscriptionStatus.Trialing, TrialEnd: not null, Customer: not null }) @@ -79,10 +81,13 @@ public class OrganizationWarningsQuery( var customer = subscription.Customer; + var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); + var hasPaymentMethod = !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || !string.IsNullOrEmpty(customer.DefaultSourceId) || - customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId); + hasUnverifiedBankAccount || + customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); if (hasPaymentMethod) { @@ -101,49 +106,58 @@ public class OrganizationWarningsQuery( Provider? provider, Subscription subscription) { - if (organization.Enabled && subscription.Status is StripeConstants.SubscriptionStatus.Trialing) - { - var isStripeCustomerWithoutPayment = - subscription.Customer.InvoiceSettings.DefaultPaymentMethodId is null; - var isBraintreeCustomer = - subscription.Customer.Metadata.ContainsKey(BraintreeCustomerIdKey); - var hasNoPaymentMethod = isStripeCustomerWithoutPayment && !isBraintreeCustomer; + var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id); - if (hasNoPaymentMethod && await currentContext.OrganizationOwner(organization.Id)) - { - return new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" }; - } - } - - if (organization.Enabled || - subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid - and not StripeConstants.SubscriptionStatus.Canceled) + switch (organization.Enabled) { - return null; - } - - if (provider != null) - { - return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; - } - - if (await currentContext.OrganizationOwner(organization.Id)) - { - return subscription.Status switch - { - StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + // Member of an enabled, trialing organization. + case true when subscription.Status is SubscriptionStatus.Trialing: { - Resolution = "add_payment_method" - }, - StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning - { - Resolution = "resubscribe" - }, - _ => null - }; - } + var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); - return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + var hasPaymentMethod = + !string.IsNullOrEmpty(subscription.Customer.InvoiceSettings.DefaultPaymentMethodId) || + !string.IsNullOrEmpty(subscription.Customer.DefaultSourceId) || + hasUnverifiedBankAccount || + subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); + + // If this member is the owner and there's no payment method on file, ask them to add one. + return isOrganizationOwner && !hasPaymentMethod + ? new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" } + : null; + } + // Member of disabled and unpaid or canceled organization. + case false when subscription.Status is SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled: + { + // If the organization is managed by a provider, return a warning asking them to contact the provider. + if (provider != null) + { + return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; + } + + /* If the organization is not managed by a provider and this user is the owner, return an action warning based + on the subscription status. */ + if (isOrganizationOwner) + { + return subscription.Status switch + { + SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + { + Resolution = "add_payment_method" + }, + SubscriptionStatus.Canceled => new InactiveSubscriptionWarning + { + Resolution = "resubscribe" + }, + _ => null + }; + } + + // Otherwise, this member is not the owner, and we need to ask them to contact the owner. + return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + } + default: return null; + } } private async Task GetResellerRenewalWarning( @@ -158,7 +172,7 @@ public class OrganizationWarningsQuery( return null; } - if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice) + if (subscription.CollectionMethod != CollectionMethod.SendInvoice) { return null; } @@ -168,8 +182,8 @@ public class OrganizationWarningsQuery( // ReSharper disable once ConvertIfStatementToSwitchStatement if (subscription is { - Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active, - LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid } + Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active, + LatestInvoice: null or { Status: InvoiceStatus.Paid } } && (subscription.CurrentPeriodEnd - now).TotalDays <= 14) { return new ResellerRenewalWarning @@ -184,8 +198,8 @@ public class OrganizationWarningsQuery( if (subscription is { - Status: StripeConstants.SubscriptionStatus.Active, - LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null } + Status: SubscriptionStatus.Active, + LatestInvoice: { Status: InvoiceStatus.Open, DueDate: not null } } && subscription.LatestInvoice.DueDate > now) { return new ResellerRenewalWarning @@ -200,7 +214,7 @@ public class OrganizationWarningsQuery( } // ReSharper disable once InvertIf - if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue) + if (subscription.Status == SubscriptionStatus.PastDue) { var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions { @@ -226,4 +240,22 @@ public class OrganizationWarningsQuery( return null; } + + private async Task HasUnverifiedBankAccount( + Organization organization) + { + var setupIntentId = await setupIntentCache.Get(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/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs rename to test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs index 3b69dc2dfc..8570dfc6be 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs @@ -14,7 +14,7 @@ using NSubstitute; using Xunit; using JsonSerializer = System.Text.Json.JsonSerializer; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Commands; [SutProviderCustomize] public class UpdateOrganizationLicenseCommandTests diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs index a81b390ae1..ed3698fb1d 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetCloudOrganizationLicenseQueryTests.cs @@ -18,7 +18,7 @@ using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SubscriptionInfoCustomize] [OrganizationLicenseCustomize] diff --git a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs similarity index 70% rename from test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 67979f506e..54c982192b 100644 --- a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -1,12 +1,13 @@ -using Bit.Api.Billing.Queries.Organizations; -using Bit.Core.AdminConsole.Entities; +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.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Services; -using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -15,17 +16,17 @@ using Stripe; using Stripe.TestHelpers; using Xunit; -namespace Bit.Api.Test.Billing.Queries.Organizations; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SutProviderCustomize] -public class OrganizationWarningsQueryTests +public class GetOrganizationWarningsQueryTests { private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"]; [Theory, BitAutoData] public async Task Run_NoSubscription_NoWarnings( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { sutProvider.GetDependency() .GetSubscription(organization, Arg.Is(options => @@ -46,7 +47,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_FreeTrialWarning( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -70,6 +71,7 @@ public class OrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); var response = await sutProvider.Sut.Run(organization); @@ -79,10 +81,90 @@ public class OrganizationWarningsQueryTests }); } + [Theory, BitAutoData] + public async Task Run_Has_FreeTrialWarning_WithUnverifiedBankAccount_NoWarning( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + const string setupIntentId = "setup_intent_id"; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + TrialEnd = now.AddDays(7), + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + sutProvider.GetDependency().Get(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() + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.FreeTrial); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethodOptionalTrial( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "add_payment_method_optional_trial" + }); + } + [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -109,7 +191,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -135,7 +217,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -161,7 +243,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { organization.Enabled = false; @@ -187,7 +269,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_Upcoming( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -225,7 +307,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_Issued( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; @@ -269,7 +351,7 @@ public class OrganizationWarningsQueryTests [Theory, BitAutoData] public async Task Run_Has_ResellerRenewalWarning_PastDue( Organization organization, - SutProvider sutProvider) + SutProvider sutProvider) { var now = DateTime.UtcNow; diff --git a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs similarity index 98% rename from test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs rename to test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs index 5d3d4acff7..f0fa3db84e 100644 --- a/test/Core.Test/Billing/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetSelfHostedOrganizationLicenseQueryTests.cs @@ -12,7 +12,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Xunit; -namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses; +namespace Bit.Core.Test.Billing.Organizations.Queries; [SutProviderCustomize] public class GetSelfHostedOrganizationLicenseQueryTests From 2cf7208eb302eda96be116d0a6675bb1cabc81bd Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:12:25 +0100 Subject: [PATCH 084/326] [PM 21897]Add Manual Enable/Disable Override for Providers in Admin Portal (#6072) * Add the changes for the enable provider * remove the wanted permission added * Added a unit testing for the updateAsync --- .../AdminConsole/Services/ProviderService.cs | 24 ++ .../Services/ProviderServiceTests.cs | 257 ++++++++++++++++++ .../Controllers/ProvidersController.cs | 13 +- .../AdminConsole/Models/ProviderEditModel.cs | 5 + .../AdminConsole/Views/Providers/Edit.cshtml | 8 + .../Views/Providers/_ViewInformation.cshtml | 6 + src/Admin/Enums/Permissions.cs | 1 + src/Admin/Utilities/RolePermissionMapping.cs | 6 +- 8 files changed, 317 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 5a0ae68631..3300b05531 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -152,7 +152,15 @@ public class ProviderService : IProviderService throw new ArgumentException("Cannot create provider this way."); } + var existingProvider = await _providerRepository.GetByIdAsync(provider.Id); + var enabledStatusChanged = existingProvider != null && existingProvider.Enabled != provider.Enabled; + await _providerRepository.ReplaceAsync(provider); + + if (enabledStatusChanged && (provider.Type == ProviderType.Msp || provider.Type == ProviderType.BusinessUnit)) + { + await UpdateClientOrganizationsEnabledStatusAsync(provider.Id, provider.Enabled); + } } public async Task> InviteUserAsync(ProviderUserInvite invite) @@ -728,4 +736,20 @@ public class ProviderService : IProviderService throw new BadRequestException($"Unsupported provider type {providerType}."); } } + + private async Task UpdateClientOrganizationsEnabledStatusAsync(Guid providerId, bool enabled) + { + var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); + + foreach (var providerOrganization in providerOrganizations) + { + var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId); + if (organization != null && organization.Enabled != enabled) + { + organization.Enabled = enabled; + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + } + } + } } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index cb8a9e8c69..608b4b3034 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; @@ -188,6 +189,262 @@ public class ProviderServiceTests await sutProvider.Sut.UpdateAsync(provider); } + [Theory, BitAutoData] + public async Task UpdateAsync_ExistingProviderIsNull_DoesNotCallUpdateClientOrganizationsEnabledStatus( + Provider provider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + + providerRepository.GetByIdAsync(provider.Id).Returns((Provider)null); + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_EnabledStatusNotChanged_DoesNotCallUpdateClientOrganizationsEnabledStatus( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = provider.Enabled; // Same enabled status + provider.Type = ProviderType.Msp; // Set to a type that would trigger update if status changed + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_EnabledStatusChangedButProviderTypeIsReseller_DoesNotCallUpdateClientOrganizationsEnabledStatus( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = !provider.Enabled; // Different enabled status + provider.Type = ProviderType.Reseller; // Type that should not trigger update + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.DidNotReceive().GetManyDetailsByProviderAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsMsp_CallsUpdateClientOrganizationsEnabledStatus( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = !provider.Enabled; // Different enabled status + provider.Type = ProviderType.Msp; // Type that should trigger update + + // Create test provider organization details + var providerOrganizationDetails = new List + { + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }, + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() } + }; + + // Create test organizations with different enabled status than what we're setting + var organizations = providerOrganizationDetails.Select(po => + { + var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled }; + return org; + }).ToList(); + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails); + + foreach (var org in organizations) + { + organizationRepository.GetByIdAsync(org.Id).Returns(org); + } + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id); + + foreach (var org in organizations) + { + await organizationRepository.Received(1).ReplaceAsync(Arg.Is(o => + o.Id == org.Id && o.Enabled == provider.Enabled)); + await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is(o => + o.Id == org.Id && o.Enabled == provider.Enabled)); + } + } + + [Theory, BitAutoData] + public async Task UpdateAsync_EnabledStatusChangedAndProviderTypeIsBusinessUnit_CallsUpdateClientOrganizationsEnabledStatus( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = !provider.Enabled; // Different enabled status + provider.Type = ProviderType.BusinessUnit; // Type that should trigger update + + // Create test provider organization details + var providerOrganizationDetails = new List + { + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }, + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() } + }; + + // Create test organizations with different enabled status than what we're setting + var organizations = providerOrganizationDetails.Select(po => + { + var org = new Organization { Id = po.OrganizationId, Enabled = !provider.Enabled }; + return org; + }).ToList(); + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails); + + foreach (var org in organizations) + { + organizationRepository.GetByIdAsync(org.Id).Returns(org); + } + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id); + + foreach (var org in organizations) + { + await organizationRepository.Received(1).ReplaceAsync(Arg.Is(o => + o.Id == org.Id && o.Enabled == provider.Enabled)); + await applicationCacheService.Received(1).UpsertOrganizationAbilityAsync(Arg.Is(o => + o.Id == org.Id && o.Enabled == provider.Enabled)); + } + } + + [Theory, BitAutoData] + public async Task UpdateAsync_OrganizationEnabledStatusAlreadyMatches_DoesNotUpdateOrganization( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = !provider.Enabled; // Different enabled status + provider.Type = ProviderType.Msp; // Type that should trigger update + + // Create test provider organization details + var providerOrganizationDetails = new List + { + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }, + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() } + }; + + // Create test organizations with SAME enabled status as what we're setting + var organizations = providerOrganizationDetails.Select(po => + { + var org = new Organization { Id = po.OrganizationId, Enabled = provider.Enabled }; + return org; + }).ToList(); + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails); + + foreach (var org in organizations) + { + organizationRepository.GetByIdAsync(org.Id).Returns(org); + } + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id); + + // Organizations should not be updated since their enabled status already matches + foreach (var org in organizations) + { + await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any()); + } + } + + [Theory, BitAutoData] + public async Task UpdateAsync_OrganizationIsNull_SkipsNullOrganization( + Provider provider, Provider existingProvider, SutProvider sutProvider) + { + // Arrange + var providerRepository = sutProvider.GetDependency(); + var providerOrganizationRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + + existingProvider.Id = provider.Id; + existingProvider.Enabled = !provider.Enabled; // Different enabled status + provider.Type = ProviderType.Msp; // Type that should trigger update + + // Create test provider organization details + var providerOrganizationDetails = new List + { + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() }, + new ProviderOrganizationOrganizationDetails { Id = Guid.NewGuid(), ProviderId = provider.Id, OrganizationId = Guid.NewGuid() } + }; + + providerRepository.GetByIdAsync(provider.Id).Returns(existingProvider); + providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id).Returns(providerOrganizationDetails); + + // Return null for all organizations + organizationRepository.GetByIdAsync(Arg.Any()).Returns((Organization)null); + + // Act + await sutProvider.Sut.UpdateAsync(provider); + + // Assert + await providerRepository.Received(1).ReplaceAsync(provider); + await providerOrganizationRepository.Received(1).GetManyDetailsByProviderAsync(provider.Id); + + // No organizations should be updated since they're all null + await organizationRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + await applicationCacheService.DidNotReceive().UpsertOrganizationAbilityAsync(Arg.Any()); + } + [Theory, BitAutoData] public async Task InviteUserAsync_ProviderIdIsInvalid_Throws(ProviderUserInvite invite, SutProvider sutProvider) { diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 4fc9556a66..df333d5d4e 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.Net; using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; +using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; @@ -51,6 +52,7 @@ public class ProvidersController : Controller private readonly IProviderBillingService _providerBillingService; private readonly IPricingClient _pricingClient; private readonly IStripeAdapter _stripeAdapter; + private readonly IAccessControlService _accessControlService; private readonly string _stripeUrl; private readonly string _braintreeMerchantUrl; private readonly string _braintreeMerchantId; @@ -70,7 +72,8 @@ public class ProvidersController : Controller IProviderBillingService providerBillingService, IWebHostEnvironment webHostEnvironment, IPricingClient pricingClient, - IStripeAdapter stripeAdapter) + IStripeAdapter stripeAdapter, + IAccessControlService accessControlService) { _organizationRepository = organizationRepository; _resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand; @@ -89,6 +92,7 @@ public class ProvidersController : Controller _stripeUrl = webHostEnvironment.GetStripeUrl(); _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); _braintreeMerchantId = globalSettings.Braintree.MerchantId; + _accessControlService = accessControlService; } [RequirePermission(Permission.Provider_List_View)] @@ -291,9 +295,14 @@ public class ProvidersController : Controller return View(oldModel); } + var originalProviderStatus = provider.Enabled; + model.ToProvider(provider); - await _providerRepository.ReplaceAsync(provider); + provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox) + ? model.Enabled : originalProviderStatus; + + await _providerService.UpdateAsync(provider); await _applicationCacheService.UpsertProviderAbilityAsync(provider); if (!provider.IsBillable()) diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 51fe4bbe64..450dfbb2fc 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -38,6 +38,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject GatewaySubscriptionUrl = gatewaySubscriptionUrl; Type = provider.Type; PayByInvoice = payByInvoice; + Enabled = provider.Enabled; if (Type == ProviderType.BusinessUnit) { @@ -78,10 +79,14 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject [Display(Name = "Enterprise Seats Minimum")] public int? EnterpriseMinimumSeats { get; set; } + [Display(Name = "Enabled")] + public bool Enabled { get; set; } + public virtual Provider ToProvider(Provider existingProvider) { existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim(); existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim(); + existingProvider.Enabled = Enabled; switch (Type) { case ProviderType.Msp: diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index d2a9ed1f62..ca4fa70ab5 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -11,6 +11,7 @@ @{ ViewData["Title"] = "Provider: " + Model.Provider.DisplayName(); var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit); + var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox); }

Provider @Model.Provider.DisplayName()

@@ -30,6 +31,13 @@
Name
@Model.Provider.DisplayName()
+ @if (canCheckEnabled && (Model.Provider.Type == ProviderType.Msp || Model.Provider.Type == ProviderType.BusinessUnit)) + { +
+ + +
+ }

Business Information

Business Name
diff --git a/src/Admin/AdminConsole/Views/Providers/_ViewInformation.cshtml b/src/Admin/AdminConsole/Views/Providers/_ViewInformation.cshtml index 5d18d7a651..81debddbeb 100644 --- a/src/Admin/AdminConsole/Views/Providers/_ViewInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/_ViewInformation.cshtml @@ -14,6 +14,12 @@
Provider Type
@(Model.Provider.Type.GetDisplayAttribute()?.GetName())
+ @if (Model.Provider.Type == ProviderType.Msp || Model.Provider.Type == ProviderType.BusinessUnit) + { +
Enabled
+
@(Model.Provider.Enabled ? "Yes" : "No")
+ } +
Created
@Model.Provider.CreationDate.ToString()
diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 704fd770bb..14b255b2b6 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -45,6 +45,7 @@ public enum Permission Provider_Edit, Provider_View, Provider_ResendEmailInvite, + Provider_CheckEnabledBox, Tools_ChargeBrainTreeCustomer, Tools_PromoteAdmin, diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index f342dfce7c..b60cf895a1 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -47,6 +47,7 @@ public static class RolePermissionMapping Permission.Provider_Create, Permission.Provider_View, Permission.Provider_ResendEmailInvite, + Permission.Provider_CheckEnabledBox, Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_PromoteAdmin, Permission.Tools_PromoteProviderServiceUser, @@ -98,6 +99,7 @@ public static class RolePermissionMapping Permission.Provider_View, Permission.Provider_Edit, Permission.Provider_ResendEmailInvite, + Permission.Provider_CheckEnabledBox, Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_PromoteAdmin, Permission.Tools_PromoteProviderServiceUser, @@ -135,7 +137,8 @@ public static class RolePermissionMapping Permission.Org_Billing_LaunchGateway, Permission.Org_RequestDelete, Permission.Provider_List_View, - Permission.Provider_View + Permission.Provider_View, + Permission.Provider_CheckEnabledBox } }, { "billing", new List @@ -173,6 +176,7 @@ public static class RolePermissionMapping Permission.Provider_Edit, Permission.Provider_View, Permission.Provider_List_View, + Permission.Provider_CheckEnabledBox, Permission.Tools_ChargeBrainTreeCustomer, Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, From 76d1a2e87557b8cf89cade6b954b792803646418 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 24 Jul 2025 11:46:16 -0400 Subject: [PATCH 085/326] [PM-23287] Enable Provider When Subscription Is Paid (#6113) * test : add tests for provider update * feat: add provider update logic and dependencies * fix: remove duplicate dependencies * refactor: updated switch logic for helper method * test: add feature flag to tests * feat: add feature flag for changes --- .../SubscriptionUpdatedHandler.cs | 55 ++- .../SubscriptionUpdatedHandlerTests.cs | 353 ++++++++++++++++++ 2 files changed, 407 insertions(+), 1 deletion(-) diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index bbc17aa3b2..702d9aaf3d 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -56,11 +56,13 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; _organizationService = organizationService; + _providerService = providerService; _stripeFacade = stripeFacade; _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand; _userService = userService; _pushNotificationService = pushNotificationService; _organizationRepository = organizationRepository; + _providerRepository = providerRepository; _schedulerFactory = schedulerFactory; _organizationEnableCommand = organizationEnableCommand; _organizationDisableCommand = organizationDisableCommand; @@ -126,13 +128,34 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } break; } + case StripeSubscriptionStatus.Active when providerId.HasValue: + { + var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + if (!providerPortalTakeover) + { + break; + } + var provider = await _providerRepository.GetByIdAsync(providerId.Value); + if (provider != null) + { + provider.Enabled = true; + await _providerService.UpdateAsync(provider); + + if (IsProviderSubscriptionNowActive(parsedEvent, subscription)) + { + // Update the CancelAtPeriodEnd subscription option to prevent the now active provider subscription from being cancelled + var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }; + await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); + } + } + break; + } case StripeSubscriptionStatus.Active: { if (userId.HasValue) { await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); } - break; } } @@ -170,6 +193,36 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } } + /// + /// Checks if the provider subscription status has changed from a non-active to an active status type + /// If the previous status is already active(active,past-due,trialing),canceled,or null, then this will return false. + /// + /// The event containing the previous subscription status + /// The current subscription status + /// A boolean that represents whether the event status has changed from a non-active status to an active status + private static bool IsProviderSubscriptionNowActive(Event parsedEvent, Subscription subscription) + { + if (parsedEvent.Data.PreviousAttributes == null) + { + return false; + } + + var previousSubscription = parsedEvent + .Data + .PreviousAttributes + .ToObject() as Subscription; + + return previousSubscription?.Status switch + { + StripeSubscriptionStatus.IncompleteExpired + or StripeSubscriptionStatus.Paused + or StripeSubscriptionStatus.Incomplete + or StripeSubscriptionStatus.Unpaid + when subscription.Status == StripeSubscriptionStatus.Active => true, + _ => false + }; + } + /// /// Removes the Password Manager coupon if the organization is removing the Secrets Manager trial. /// Only applies to organizations that have a subscription from the Secrets Manager trial. diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index ce4ee608cc..f230b87dea 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -17,6 +17,7 @@ using Bit.Core.Services; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Quartz; using Stripe; using Stripe.TestHelpers; @@ -54,8 +55,10 @@ public class SubscriptionUpdatedHandlerTests _stripeFacade = Substitute.For(); _organizationSponsorshipRenewCommand = Substitute.For(); _userService = Substitute.For(); + _providerService = Substitute.For(); _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); _schedulerFactory = Substitute.For(); _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); @@ -663,4 +666,354 @@ public class SubscriptionUpdatedHandlerTests await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId); await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id); } + + [Theory] + [MemberData(nameof(GetNonActiveSubscriptions))] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasNonActive_EnableProviderAndUpdateSubscription( + Subscription previousSubscription) + { + // Arrange + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _stripeFacade + .UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(newSubscription); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .Received(1) + .UpdateSubscription(newSubscription.Id, + Arg.Is(options => options.CancelAtPeriodEnd == false)); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Canceled }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasAlreadyActive_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Active }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasTrailing_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Trialing }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasPastDue_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.PastDue }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(Arg.Any()); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task HandleAsync_ActiveProviderSubscriptionEvent_AndProviderDoesNotExist_NoChanges() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid }; + var (providerId, newSubscription, _, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .ReturnsNull(); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(providerId); + await _providerService + .DidNotReceive() + .UpdateAsync(Arg.Any()); + await _stripeFacade + .DidNotReceive() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task HandleAsync_ActiveProviderSubscriptionEvent_WithNoPreviousAttributes_EnableProvider() + { + // Arrange + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(null); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(Arg.Any()); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceive() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + private static (Guid providerId, Subscription newSubscription, Provider provider, Event parsedEvent) + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(Subscription? previousSubscription) + { + var providerId = Guid.NewGuid(); + var newSubscription = new Subscription + { + Id = previousSubscription?.Id ?? "sub_123", + Status = StripeSubscriptionStatus.Active, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + }; + + var provider = new Provider { Id = providerId, Enabled = false }; + var parsedEvent = new Event + { + Data = new EventData + { + Object = newSubscription, + PreviousAttributes = + previousSubscription == null ? null : JObject.FromObject(previousSubscription) + } + }; + return (providerId, newSubscription, provider, parsedEvent); + } + + public static IEnumerable GetNonActiveSubscriptions() + { + return new List + { + 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.Paused }, }, + }; + } } From 05398ad8a4ba3329cdf9c0aed3e4b35fa150a4a9 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:49:15 -0400 Subject: [PATCH 086/326] [PM-22736] Send password hasher (#6112) * feat: - Add SendPasswordHasher class and interface - DI for SendPasswordHasher to use Marker class allowing us to use custom options for the SendPasswordHasher without impacting other PasswordHashers. * test: Unit tests for SendPasswordHasher implementation * doc: docs for interface and comments Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> --- .../PasswordValidationConstants.cs | 6 + .../Sends/ISendPasswordHasher.cs | 21 ++++ .../KeyManagement/Sends/SendPasswordHasher.cs | 35 ++++++ .../Sends/SendPasswordHasherMarker.cs | 10 ++ ...sswordHasherServiceCollectionExtensions.cs | 31 ++++++ .../Utilities/ServiceCollectionExtensions.cs | 3 +- .../KeyManagement/SendPasswordHasherTests.cs | 103 ++++++++++++++++++ 7 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/Core/Auth/UserFeatures/PasswordValidation/PasswordValidationConstants.cs create mode 100644 src/Core/KeyManagement/Sends/ISendPasswordHasher.cs create mode 100644 src/Core/KeyManagement/Sends/SendPasswordHasher.cs create mode 100644 src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs create mode 100644 src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs create mode 100644 test/Core.Test/KeyManagement/SendPasswordHasherTests.cs diff --git a/src/Core/Auth/UserFeatures/PasswordValidation/PasswordValidationConstants.cs b/src/Core/Auth/UserFeatures/PasswordValidation/PasswordValidationConstants.cs new file mode 100644 index 0000000000..b0803cd3cd --- /dev/null +++ b/src/Core/Auth/UserFeatures/PasswordValidation/PasswordValidationConstants.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Auth.UserFeatures.PasswordValidation; + +public static class PasswordValidationConstants +{ + public const int PasswordHasherKdfIterations = 100000; +} diff --git a/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs b/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs new file mode 100644 index 0000000000..63bb7f5499 --- /dev/null +++ b/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs @@ -0,0 +1,21 @@ +namespace Bit.Core.KeyManagement.Sends; + +public interface ISendPasswordHasher +{ + /// + /// Matches the send password hash against the user provided client password hash. The send password is server hashed and the client + /// password hash is hashed by the server for comparison in this method. + /// + /// The send password that is hashed by the server. + /// The user provided password hash that has not yet been hashed by the server for comparison. + /// true if hashes match false otherwise + /// Thrown if the server password hash or client password hash is null or empty. + bool PasswordHashMatches(string sendPasswordHash, string clientPasswordHash); + + /// + /// Accepts a client hashed send password and returns a server hashed password. + /// + /// + /// server hashed password + string HashOfClientPasswordHash(string clientHashedPassword); +} diff --git a/src/Core/KeyManagement/Sends/SendPasswordHasher.cs b/src/Core/KeyManagement/Sends/SendPasswordHasher.cs new file mode 100644 index 0000000000..abe57d3cc6 --- /dev/null +++ b/src/Core/KeyManagement/Sends/SendPasswordHasher.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.KeyManagement.Sends; + +internal class SendPasswordHasher(IPasswordHasher passwordHasher) : ISendPasswordHasher +{ + private readonly IPasswordHasher _passwordHasher = passwordHasher; + + /// + /// + /// + public bool PasswordHashMatches(string sendPasswordHash, string inputPasswordHash) + { + if (string.IsNullOrWhiteSpace(sendPasswordHash) || string.IsNullOrWhiteSpace(inputPasswordHash)) + { + return false; + } + + var passwordResult = _passwordHasher.VerifyHashedPassword(SendPasswordHasherMarker.Instance, sendPasswordHash, inputPasswordHash); + + /* + In our use-case we input a high-entropy, pre-hashed secret sent by the client. Thus, we don't really care + about if the hash needs to be rehashed. Sends also only live for 30 days max. + */ + return passwordResult is PasswordVerificationResult.Success or PasswordVerificationResult.SuccessRehashNeeded; + } + + /// + /// + /// + public string HashOfClientPasswordHash(string clientHashedPassword) + { + return _passwordHasher.HashPassword(SendPasswordHasherMarker.Instance, clientHashedPassword); + } +} diff --git a/src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs b/src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs new file mode 100644 index 0000000000..d4b80a09a2 --- /dev/null +++ b/src/Core/KeyManagement/Sends/SendPasswordHasherMarker.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.KeyManagement.Sends; + +// This should not be used except for DI as open generic marker class for use with +// the SendPasswordHasher. +public class SendPasswordHasherMarker +{ + // We know we will pass a single instance that isn't used to the PasswordHasher so we + // gain an efficiency benefit of not creating multiple marker classes. + public static readonly SendPasswordHasherMarker Instance = new(); +} diff --git a/src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs b/src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs new file mode 100644 index 0000000000..22939ce60c --- /dev/null +++ b/src/Core/KeyManagement/Sends/SendPasswordHasherServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Bit.Core.Auth.UserFeatures.PasswordValidation; +using Bit.Core.KeyManagement.Sends; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +using Microsoft.Extensions.Options; + +public static class SendPasswordHasherServiceCollectionExtensions +{ + public static void AddSendPasswordServices(this IServiceCollection services) + { + const string sendPasswordHasherMarkerName = "SendPasswordHasherMarker"; + + services.AddOptions(sendPasswordHasherMarkerName) + .Configure(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations); + + services.TryAddScoped>(sp => + { + var opts = sp + .GetRequiredService>() + .Get(sendPasswordHasherMarkerName); + + var optionsAccessor = Options.Create(opts); + + return new PasswordHasher(optionsAccessor); + }); + services.TryAddScoped(); + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 0768aa7060..0c3f0cbca1 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Auth.Services.Implementations; using Bit.Core.Auth.UserFeatures; +using Bit.Core.Auth.UserFeatures.PasswordValidation; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.TrialInitiation; @@ -381,7 +382,7 @@ public static class ServiceCollectionExtensions services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>)); services.AddScoped(); - services.Configure(options => options.IterationCount = 100000); + services.Configure(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations); services.Configure(options => { options.TokenLifespan = TimeSpan.FromDays(30); diff --git a/test/Core.Test/KeyManagement/SendPasswordHasherTests.cs b/test/Core.Test/KeyManagement/SendPasswordHasherTests.cs new file mode 100644 index 0000000000..53c518b03f --- /dev/null +++ b/test/Core.Test/KeyManagement/SendPasswordHasherTests.cs @@ -0,0 +1,103 @@ +using Bit.Core.KeyManagement.Sends; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Sends; + +[SutProviderCustomize] +public class SendPasswordHasherTests +{ + [Theory] + [BitAutoData(PasswordVerificationResult.Success)] + [BitAutoData(PasswordVerificationResult.SuccessRehashNeeded)] + void VerifyPasswordHash_WithValidMatching_ReturnsTrue( + PasswordVerificationResult passwordVerificationResult, + SutProvider sutProvider, + string sendPasswordHash, + string inputPasswordHash) + { + // Arrange + sutProvider.GetDependency>() + .VerifyHashedPassword(Arg.Any(), sendPasswordHash, inputPasswordHash) + .Returns(passwordVerificationResult); + + // Act + var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash); + + // Assert + Assert.True(result); + sutProvider.GetDependency>() + .Received(1) + .VerifyHashedPassword(Arg.Any(), sendPasswordHash, inputPasswordHash); + } + + [Theory, BitAutoData] + void VerifyPasswordHash_WithNonMatchingPasswords_ReturnsFalse( + SutProvider sutProvider, + string sendPasswordHash, + string inputPasswordHash) + { + // Arrange + sutProvider.GetDependency>() + .VerifyHashedPassword(Arg.Any(), sendPasswordHash, inputPasswordHash) + .Returns(PasswordVerificationResult.Failed); + + // Act + var result = sutProvider.Sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash); + + // Assert + Assert.False(result); + sutProvider.GetDependency>() + .Received(1) + .VerifyHashedPassword(Arg.Any(), sendPasswordHash, inputPasswordHash); + } + + [Theory] + [InlineData(null, "inputPassword")] + [InlineData("", "inputPassword")] + [InlineData(" ", "inputPassword")] + [InlineData("sendPassword", null)] + [InlineData("sendPassword", "")] + [InlineData("sendPassword", " ")] + [InlineData(null, null)] + [InlineData("", "")] + public void VerifyPasswordHash_WithNullOrEmptyParameters_ReturnsFalse( + string? sendPasswordHash, + string? inputPasswordHash) + { + // Arrange + var passwordHasher = Substitute.For>(); + var sut = new SendPasswordHasher(passwordHasher); + + // Act + var result = sut.PasswordHashMatches(sendPasswordHash, inputPasswordHash); + + // Assert + Assert.False(result); + passwordHasher.DidNotReceive().VerifyHashedPassword(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + void HashPasswordHash_WithValidInput_ReturnsHashedPassword( + SutProvider sutProvider, + string clientHashedPassword, + string expectedHashedResult) + { + // Arrange + sutProvider.GetDependency>() + .HashPassword(Arg.Any(), clientHashedPassword) + .Returns(expectedHashedResult); + + // Act + var result = sutProvider.Sut.HashOfClientPasswordHash(clientHashedPassword); + + // Assert + Assert.Equal(expectedHashedResult, result); + sutProvider.GetDependency>() + .Received(1) + .HashPassword(Arg.Any(), clientHashedPassword); + } +} From c503ecbefc56b1c242f6fcbf660de6ac0970260f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:50:09 -0500 Subject: [PATCH 087/326] [PM-21827] Implement mechanism to suspend currently unpaid providers (#6119) * Manually suspend provider and set cancel_at when we receive 'suspend_provider' metadata update * Run dotnet format' --- .../SubscriptionUpdatedHandler.cs | 87 ++++-- .../SubscriptionUpdatedHandlerTests.cs | 248 +++++++++--------- 2 files changed, 198 insertions(+), 137 deletions(-) diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 702d9aaf3d..d5fcfb20d4 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,4 +1,5 @@ -using Bit.Billing.Constants; +using System.Globalization; +using Bit.Billing.Constants; using Bit.Billing.Jobs; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; @@ -316,7 +317,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private async Task HandleUnpaidProviderSubscriptionAsync( Guid providerId, Event parsedEvent, - Subscription subscription) + Subscription currentSubscription) { var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); @@ -338,26 +339,43 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler if (parsedEvent.Data.PreviousAttributes != null) { - if (parsedEvent.Data.PreviousAttributes.ToObject() as Subscription is - { - Status: - StripeSubscriptionStatus.Trialing or - StripeSubscriptionStatus.Active or - StripeSubscriptionStatus.PastDue - } && subscription is - { - Status: StripeSubscriptionStatus.Unpaid, - LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create" - }) + var previousSubscription = parsedEvent.Data.PreviousAttributes.ToObject() as Subscription; + + var updateIsSubscriptionGoingUnpaid = previousSubscription is { - if (subscription.TestClock != null) + Status: + StripeSubscriptionStatus.Trialing or + StripeSubscriptionStatus.Active or + StripeSubscriptionStatus.PastDue + } && currentSubscription is + { + Status: StripeSubscriptionStatus.Unpaid, + LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create" + }; + + var updateIsManualSuspensionViaMetadata = CheckForManualSuspensionViaMetadata( + previousSubscription, currentSubscription); + + if (updateIsSubscriptionGoingUnpaid || updateIsManualSuspensionViaMetadata) + { + if (currentSubscription.TestClock != null) { - await WaitForTestClockToAdvanceAsync(subscription.TestClock); + await WaitForTestClockToAdvanceAsync(currentSubscription.TestClock); } - var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; - await _stripeFacade.UpdateSubscription(subscription.Id, - new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) }); + var now = currentSubscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) }; + + if (updateIsManualSuspensionViaMetadata) + { + subscriptionUpdateOptions.Metadata = new Dictionary + { + ["suspended_provider_via_webhook_at"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture) + }; + } + + await _stripeFacade.UpdateSubscription(currentSubscription.Id, subscriptionUpdateOptions); } } } @@ -379,4 +397,37 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } } } + + private static bool CheckForManualSuspensionViaMetadata( + Subscription? previousSubscription, + Subscription currentSubscription) + { + /* + * When metadata on a subscription is updated, we'll receive an event that has: + * Previous Metadata: { newlyAddedKey: null } + * Current Metadata: { newlyAddedKey: newlyAddedValue } + * + * As such, our check for a manual suspension must ensure that the 'previous_attributes' does contain the + * 'metadata' property, but also that the "suspend_provider" key in that metadata is set to null. + * + * If we don't do this and instead do a null coalescing check on 'previous_attributes?.metadata?.TryGetValue', + * we'll end up marking an event where 'previous_attributes.metadata' = null (which could be any subscription update + * that does not update the metadata) the same as a manual suspension. + */ + const string key = "suspend_provider"; + + if (previousSubscription is not { Metadata: not null } || + !previousSubscription.Metadata.TryGetValue(key, out var previousValue)) + { + return false; + } + + if (previousValue == null) + { + return !string.IsNullOrEmpty( + currentSubscription.Metadata.TryGetValue(key, out var currentValue) ? currentValue : null); + } + + return false; + } } diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index f230b87dea..0d1f54ecfd 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -20,7 +20,6 @@ using NSubstitute; using NSubstitute.ReturnsExtensions; using Quartz; using Stripe; -using Stripe.TestHelpers; using Xunit; using Event = Stripe.Event; @@ -36,14 +35,12 @@ public class SubscriptionUpdatedHandlerTests private readonly IUserService _userService; private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; - private readonly ISchedulerFactory _schedulerFactory; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; private readonly IPricingClient _pricingClient; private readonly IFeatureService _featureService; private readonly IProviderRepository _providerRepository; private readonly IProviderService _providerService; - private readonly ILogger _logger; private readonly IScheduler _scheduler; private readonly SubscriptionUpdatedHandler _sut; @@ -58,18 +55,17 @@ public class SubscriptionUpdatedHandlerTests _providerService = Substitute.For(); _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); - _providerRepository = Substitute.For(); - _schedulerFactory = Substitute.For(); + var schedulerFactory = Substitute.For(); _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); _pricingClient = Substitute.For(); _featureService = Substitute.For(); _providerRepository = Substitute.For(); _providerService = Substitute.For(); - _logger = Substitute.For>(); + var logger = Substitute.For>(); _scheduler = Substitute.For(); - _schedulerFactory.GetScheduler().Returns(_scheduler); + schedulerFactory.GetScheduler().Returns(_scheduler); _sut = new SubscriptionUpdatedHandler( _stripeEventService, @@ -80,14 +76,14 @@ public class SubscriptionUpdatedHandlerTests _userService, _pushNotificationService, _organizationRepository, - _schedulerFactory, + schedulerFactory, _organizationEnableCommand, _organizationDisableCommand, _pricingClient, _featureService, _providerRepository, _providerService, - _logger); + logger); } [Fact] @@ -126,61 +122,54 @@ public class SubscriptionUpdatedHandlerTests } [Fact] - public async Task HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation() + public async Task + HandleAsync_UnpaidProviderSubscription_WithManualSuspensionViaMetadata_DisablesProviderAndSchedulesCancellation() { // Arrange var providerId = Guid.NewGuid(); - const string subscriptionId = "sub_123"; - var frozenTime = DateTime.UtcNow; + var subscriptionId = "sub_test123"; - var testClock = new TestClock + var previousSubscription = new Subscription { - Id = "clock_123", - Status = "ready", - FrozenTime = frozenTime + Id = subscriptionId, + Status = StripeSubscriptionStatus.Active, + Metadata = new Dictionary + { + ["suspend_provider"] = null // This is the key part - metadata exists, but value is null + } }; - var subscription = new Subscription + var currentSubscription = new Subscription { Id = subscriptionId, Status = StripeSubscriptionStatus.Unpaid, - Metadata = new Dictionary { { "providerId", providerId.ToString() } }, - LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }, - TestClock = testClock - }; - - var provider = new Provider - { - Id = providerId, - Name = "Test Provider", - Enabled = true + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), + Metadata = new Dictionary + { + ["providerId"] = providerId.ToString(), + ["suspend_provider"] = "true" // Now has a value, indicating manual suspension + }, + TestClock = null }; var parsedEvent = new Event { + Id = "evt_test123", + Type = HandledStripeWebhook.SubscriptionUpdated, Data = new EventData { - PreviousAttributes = JObject.FromObject(new - { - status = "active" - }) + Object = currentSubscription, + PreviousAttributes = JObject.FromObject(previousSubscription) } }; - _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) - .Returns(subscription); + var provider = new Provider { Id = providerId, Enabled = true }; - _stripeEventUtilityService.GetIdsFromMetadata(Arg.Any>()) + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true); + _stripeEventService.GetSubscription(parsedEvent, true, Arg.Any>()).Returns(currentSubscription); + _stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata) .Returns(Tuple.Create(null, null, providerId)); - - _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) - .Returns(true); - - _providerRepository.GetByIdAsync(providerId) - .Returns(provider); - - _stripeFacade.GetTestClock(testClock.Id) - .Returns(testClock); + _providerRepository.GetByIdAsync(providerId).Returns(provider); // Act await _sut.HandleAsync(parsedEvent); @@ -188,8 +177,75 @@ public class SubscriptionUpdatedHandlerTests // Assert Assert.False(provider.Enabled); await _providerService.Received(1).UpdateAsync(provider); - await _stripeFacade.Received(1).UpdateSubscription(subscriptionId, - Arg.Is(o => o.CancelAt == frozenTime.AddDays(7))); + + // Verify that UpdateSubscription was called with both CancelAt and the new metadata + await _stripeFacade.Received(1).UpdateSubscription( + subscriptionId, + Arg.Is(options => + options.CancelAt.HasValue && + options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) && + options.Metadata != null && + options.Metadata.ContainsKey("suspended_provider_via_webhook_at"))); + } + + [Fact] + public async Task + HandleAsync_UnpaidProviderSubscription_WithValidTransition_DisablesProviderAndSchedulesCancellation() + { + // Arrange + var providerId = Guid.NewGuid(); + var subscriptionId = "sub_test123"; + + var previousSubscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Active, + Metadata = new Dictionary { ["providerId"] = providerId.ToString() } + }; + + var currentSubscription = new Subscription + { + Id = subscriptionId, + Status = StripeSubscriptionStatus.Unpaid, + CurrentPeriodEnd = DateTime.UtcNow.AddDays(30), + Metadata = new Dictionary { ["providerId"] = providerId.ToString() }, + LatestInvoice = new Invoice { BillingReason = "subscription_cycle" }, + TestClock = null + }; + + var parsedEvent = new Event + { + Id = "evt_test123", + Type = HandledStripeWebhook.SubscriptionUpdated, + Data = new EventData + { + Object = currentSubscription, + PreviousAttributes = JObject.FromObject(previousSubscription) + } + }; + + var provider = new Provider { Id = providerId, Enabled = true }; + + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover).Returns(true); + _stripeEventService.GetSubscription(parsedEvent, true, Arg.Any>()).Returns(currentSubscription); + _stripeEventUtilityService.GetIdsFromMetadata(currentSubscription.Metadata) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository.GetByIdAsync(providerId).Returns(provider); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + Assert.False(provider.Enabled); + await _providerService.Received(1).UpdateAsync(provider); + + // Verify that UpdateSubscription was called with CancelAt but WITHOUT suspension metadata + await _stripeFacade.Received(1).UpdateSubscription( + subscriptionId, + Arg.Is(options => + options.CancelAt.HasValue && + options.CancelAt.Value <= DateTime.UtcNow.AddDays(7).AddMinutes(1) && + (options.Metadata == null || !options.Metadata.ContainsKey("suspended_provider_via_webhook_at")))); } [Fact] @@ -207,12 +263,7 @@ public class SubscriptionUpdatedHandlerTests LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; - var provider = new Provider - { - Id = providerId, - Name = "Test Provider", - Enabled = true - }; + var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true }; var parsedEvent = new Event { @@ -220,7 +271,7 @@ public class SubscriptionUpdatedHandlerTests { PreviousAttributes = JObject.FromObject(new { - status = "unpaid" // No valid transition + status = "unpaid" // No valid transition }) } }; @@ -261,20 +312,9 @@ public class SubscriptionUpdatedHandlerTests LatestInvoice = new Invoice { BillingReason = "subscription_cycle" } }; - var provider = new Provider - { - Id = providerId, - Name = "Test Provider", - Enabled = true - }; + var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true }; - var parsedEvent = new Event - { - Data = new EventData - { - PreviousAttributes = null - } - }; + var parsedEvent = new Event { Data = new EventData { PreviousAttributes = null } }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) .Returns(subscription); @@ -314,12 +354,7 @@ public class SubscriptionUpdatedHandlerTests LatestInvoice = new Invoice { BillingReason = "renewal" } }; - var provider = new Provider - { - Id = providerId, - Name = "Test Provider", - Enabled = true - }; + var provider = new Provider { Id = providerId, Name = "Test Provider", Enabled = true }; var parsedEvent = new Event { Data = new EventData() }; @@ -434,10 +469,10 @@ public class SubscriptionUpdatedHandlerTests Metadata = new Dictionary { { "userId", userId.ToString() } }, Items = new StripeList { - Data = new List - { - new() { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } } - } + Data = + [ + new SubscriptionItem { Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId } } + ] } }; @@ -478,11 +513,7 @@ public class SubscriptionUpdatedHandlerTests Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; - var organization = new Organization - { - Id = organizationId, - PlanType = PlanType.EnterpriseAnnually2023 - }; + var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 }; var parsedEvent = new Event { Data = new EventData() }; _stripeEventService.GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) @@ -495,7 +526,7 @@ public class SubscriptionUpdatedHandlerTests .Returns(organization); _stripeFacade.ListInvoices(Arg.Any()) - .Returns(new StripeList { Data = new List { new Invoice { Id = "inv_123" } } }); + .Returns(new StripeList { Data = [new Invoice { Id = "inv_123" }] }); var plan = new Enterprise2023Plan(true); _pricingClient.GetPlanOrThrow(organization.PlanType) @@ -577,7 +608,8 @@ public class SubscriptionUpdatedHandlerTests } [Fact] - public async Task HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon() + public async Task + HandleAsync_WhenSubscriptionIsActive_AndOrganizationHasSecretsManagerTrial_AndRemovingSecretsManagerTrial_RemovesPasswordManagerCoupon() { // Arrange var organizationId = Guid.NewGuid(); @@ -589,34 +621,18 @@ public class SubscriptionUpdatedHandlerTests CustomerId = "cus_123", Items = new StripeList { - Data = new List - { - new() { Plan = new Stripe.Plan { Id = "2023-enterprise-org-seat-annually" } } - } + Data = [new SubscriptionItem { 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" } } }, - Discount = new Discount - { - Coupon = new Coupon { Id = "sm-standalone" } - }, - Metadata = new Dictionary - { - { "organizationId", organizationId.ToString() } - } + Discount = new Discount { Coupon = new Coupon { Id = "sm-standalone" } }, + Metadata = new Dictionary { { "organizationId", organizationId.ToString() } } }; - var organization = new Organization - { - Id = organizationId, - PlanType = PlanType.EnterpriseAnnually2023 - }; + var organization = new Organization { Id = organizationId, PlanType = PlanType.EnterpriseAnnually2023 }; var plan = new Enterprise2023Plan(true); _pricingClient.GetPlanOrThrow(organization.PlanType) @@ -631,20 +647,14 @@ public class SubscriptionUpdatedHandlerTests { items = new { - data = new[] - { - new { plan = new { id = "secrets-manager-enterprise-seat-annually" } } - } + data = new[] { new { plan = new { id = "secrets-manager-enterprise-seat-annually" } } } }, Items = new StripeList { - Data = new List - { - new SubscriptionItem - { - Plan = new Stripe.Plan { Id = "secrets-manager-enterprise-seat-annually" } - } - } + Data = + [ + new SubscriptionItem { Plan = new Plan { Id = "secrets-manager-enterprise-seat-annually" } } + ] } }) } @@ -990,7 +1000,7 @@ public class SubscriptionUpdatedHandlerTests { Id = previousSubscription?.Id ?? "sub_123", Status = StripeSubscriptionStatus.Active, - Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + Metadata = new Dictionary { { "providerId", providerId.ToString() } } }; var provider = new Provider { Id = providerId, Enabled = false }; @@ -1010,10 +1020,10 @@ public class SubscriptionUpdatedHandlerTests { return new List { - 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.Paused }, }, + 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.Paused } } }; } } From 571111e8974b26e770efe0dd40ddfa8c20ee2ecf Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 25 Jul 2025 10:14:16 -0400 Subject: [PATCH 088/326] [PM-18239] Master password policy requirement (#5936) * wip * initial implementation * add tests * more tests, fix policy Enabled * remove exempt statuses * test EnforcedOptions is populated * clean up, add test * fix test, add json attributes for deserialization * fix attribute casing * fix test --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../Policies/MasterPasswordPolicyData.cs | 12 ++- .../MasterPasswordPolicyRequirement.cs | 55 ++++++++++++++ .../Services/Implementations/PolicyService.cs | 23 +++++- .../Controllers/PoliciesControllerTests.cs | 14 ++-- .../MasterPasswordPolicyRequirementTests.cs | 75 +++++++++++++++++++ .../Services/PolicyServiceTests.cs | 38 ++++++++++ 6 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs index f2f275b708..b66244ba5f 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs @@ -1,20 +1,28 @@ -namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using System.Text.Json.Serialization; +namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; public class MasterPasswordPolicyData : IPolicyDataModel { + [JsonPropertyName("minComplexity")] public int? MinComplexity { get; set; } + [JsonPropertyName("minLength")] public int? MinLength { get; set; } + [JsonPropertyName("requireLower")] public bool? RequireLower { get; set; } + [JsonPropertyName("requireUpper")] public bool? RequireUpper { get; set; } + [JsonPropertyName("requireNumbers")] public bool? RequireNumbers { get; set; } + [JsonPropertyName("requireSpecial")] public bool? RequireSpecial { get; set; } + [JsonPropertyName("enforceOnLogin")] public bool? EnforceOnLogin { get; set; } /// /// Combine the other policy data with this instance, taking the most secure options /// /// The other policy instance to combine with this - public void CombineWith(MasterPasswordPolicyData other) + public void CombineWith(MasterPasswordPolicyData? other) { if (other == null) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs new file mode 100644 index 0000000000..9e8154db53 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirement.cs @@ -0,0 +1,55 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Master Password Requirements policy. +/// +public class MasterPasswordPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether MasterPassword requirements are enabled for the user. + /// + public bool Enabled { get; init; } + + /// + /// Master Password Policy data model associated with this Policy + /// + public MasterPasswordPolicyData? EnforcedOptions { get; init; } +} + +public class MasterPasswordPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.MasterPassword; + + protected override bool ExemptProviders => false; + + protected override IEnumerable ExemptRoles => []; + + protected override IEnumerable ExemptStatuses => + [OrganizationUserStatusType.Accepted, + OrganizationUserStatusType.Invited, + OrganizationUserStatusType.Revoked, + ]; + + public override MasterPasswordPolicyRequirement Create(IEnumerable policyDetails) + { + var result = policyDetails + .Select(p => p.GetDataModel()) + .Aggregate( + new MasterPasswordPolicyRequirement(), + (result, data) => + { + data.CombineWith(result.EnforcedOptions); + return new MasterPasswordPolicyRequirement + { + Enabled = true, + EnforcedOptions = data + }; + }); + + return result; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs index 5ba39e8054..a83eccc301 100644 --- a/src/Core/AdminConsole/Services/Implementations/PolicyService.cs +++ b/src/Core/AdminConsole/Services/Implementations/PolicyService.cs @@ -3,6 +3,8 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; @@ -19,21 +21,39 @@ public class PolicyService : IPolicyService private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPolicyRepository _policyRepository; private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public PolicyService( IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, IPolicyRepository policyRepository, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) { _applicationCacheService = applicationCacheService; _organizationUserRepository = organizationUserRepository; _policyRepository = policyRepository; _globalSettings = globalSettings; + _featureService = featureService; + _policyRequirementQuery = policyRequirementQuery; } public async Task GetMasterPasswordPolicyForUserAsync(User user) { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var masterPaswordPolicy = (await _policyRequirementQuery.GetAsync(user.Id)); + + if (!masterPaswordPolicy.Enabled) + { + return null; + } + + return masterPaswordPolicy.EnforcedOptions; + } + var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id)) .Where(p => p.Type == PolicyType.MasterPassword && p.Enabled) .ToList(); @@ -51,6 +71,7 @@ public class PolicyService : IPolicyService } return enforcedOptions; + } public async Task> GetPoliciesApplicableToUserAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted) diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index 1f652c80f5..f5f3eddd3b 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -73,13 +73,13 @@ public class PoliciesControllerTests // Assert that the data is deserialized correctly into a Dictionary // for all MasterPasswordPolicyData properties - Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["MinComplexity"]).GetInt32()); - Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["MinLength"]).GetInt32()); - Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["RequireLower"]).GetBoolean()); - Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["RequireUpper"]).GetBoolean()); - Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["RequireNumbers"]).GetBoolean()); - Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["RequireSpecial"]).GetBoolean()); - Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["EnforceOnLogin"]).GetBoolean()); + Assert.Equal(mpPolicyData.MinComplexity, ((JsonElement)result.Data["minComplexity"]).GetInt32()); + Assert.Equal(mpPolicyData.MinLength, ((JsonElement)result.Data["minLength"]).GetInt32()); + Assert.Equal(mpPolicyData.RequireLower, ((JsonElement)result.Data["requireLower"]).GetBoolean()); + Assert.Equal(mpPolicyData.RequireUpper, ((JsonElement)result.Data["requireUpper"]).GetBoolean()); + Assert.Equal(mpPolicyData.RequireNumbers, ((JsonElement)result.Data["requireNumbers"]).GetBoolean()); + Assert.Equal(mpPolicyData.RequireSpecial, ((JsonElement)result.Data["requireSpecial"]).GetBoolean()); + Assert.Equal(mpPolicyData.EnforceOnLogin, ((JsonElement)result.Data["enforceOnLogin"]).GetBoolean()); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs new file mode 100644 index 0000000000..d3991bcde7 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/MasterPasswordPolicyRequirementTests.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class MasterPasswordPolicyRequirementFactoryTests +{ + [Theory, BitAutoData] + public void MasterPasswordPolicyData_CombineWith_Joins_Policy_Options(SutProvider sutProvider) + { + var mpd1 = JsonSerializer.Serialize(new MasterPasswordPolicyData { MinLength = 20, RequireLower = false, RequireSpecial = false }); + var mpd2 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireLower = true }); + var mpd3 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireSpecial = true }); + + var policyDetails1 = new PolicyDetails + { + PolicyType = PolicyType.MasterPassword, + PolicyData = mpd1 + }; + + var policyDetails2 = new PolicyDetails + { + PolicyType = PolicyType.MasterPassword, + PolicyData = mpd2 + }; + var policyDetails3 = new PolicyDetails + { + PolicyType = PolicyType.MasterPassword, + PolicyData = mpd3 + }; + + + var actual = sutProvider.Sut.Create([policyDetails1, policyDetails2, policyDetails3]); + + Assert.NotNull(actual); + Assert.True(actual.Enabled); + Assert.True(actual.EnforcedOptions.RequireLower); + Assert.True(actual.EnforcedOptions.RequireSpecial); + Assert.Equal(20, actual.EnforcedOptions.MinLength); + } + + [Theory, BitAutoData] + public void MasterPassword_IsFalse_IfNoPolicies(SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.Enabled); + Assert.Null(actual.EnforcedOptions); + } + + [Theory, BitAutoData] + public void MasterPassword_IsTrue_IfAnyDisableSendPolicies( + [PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create(policies); + + Assert.True(actual.Enabled); + Assert.NotNull(actual.EnforcedOptions); + Assert.NotNull(actual.EnforcedOptions.EnforceOnLogin); + Assert.NotNull(actual.EnforcedOptions.RequireLower); + Assert.NotNull(actual.EnforcedOptions.RequireNumbers); + Assert.NotNull(actual.EnforcedOptions.RequireSpecial); + Assert.NotNull(actual.EnforcedOptions.RequireUpper); + Assert.Null(actual.EnforcedOptions.MinComplexity); + Assert.Null(actual.EnforcedOptions.MinLength); + } +} diff --git a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs index 62ab584c4b..0af9eef12e 100644 --- a/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/PolicyServiceTests.cs @@ -1,9 +1,15 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services.Implementations; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -117,6 +123,38 @@ public class PolicyServiceTests Assert.True(result); } + [Theory, BitAutoData] + public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagEnabled_EvaluatesPolicyRequirement(User user, SutProvider sutProvider) + { + SetupUserPolicies(user.Id, sutProvider); + var policyRequirement = new MasterPasswordPolicyRequirement + { + Enabled = true, + EnforcedOptions = new MasterPasswordPolicyData() + }; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + sutProvider.GetDependency().GetAsync(user.Id).Returns(policyRequirement); + + var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements); + await sutProvider.GetDependency().DidNotReceive().GetManyByUserIdAsync(user.Id); + await sutProvider.GetDependency().Received(1).GetAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task GetMasterPasswordPolicyForUserAsync_WithFeatureFlagDisabled_EvaluatesPolicyDetails(User user, SutProvider sutProvider) + { + SetupUserPolicies(user.Id, sutProvider); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false); + + var result = await sutProvider.Sut.GetMasterPasswordPolicyForUserAsync(user); + + sutProvider.GetDependency().Received(1).IsEnabled(FeatureFlagKeys.PolicyRequirements); + await sutProvider.GetDependency().Received(1).GetManyByUserIdAsync(user.Id); + await sutProvider.GetDependency().DidNotReceive().GetAsync(user.Id); + } + private static void SetupOrg(SutProvider sutProvider, Guid organizationId, Organization organization) { sutProvider.GetDependency() From 04d66a54a4fc9e8df482d45df7dd1ac645ceb14f Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 25 Jul 2025 11:55:34 -0400 Subject: [PATCH 089/326] register MasterPasswordPolicyRequirementFactory (#6125) --- .../Policies/PolicyServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 87fdcbe543..e31e9d44c9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -37,5 +37,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, OrganizationDataOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); services.AddScoped, RequireTwoFactorPolicyRequirementFactory>(); + services.AddScoped, MasterPasswordPolicyRequirementFactory>(); } } From 7e80e01747617f3d88168570f1ee50a90ea31772 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:44:51 -0400 Subject: [PATCH 090/326] [PM-21948] Warn on deprecated logging methods (#6101) * Add warnings and scaffold tests * Do some private reflection * Add tests for warnings * Add explainer comment * Remove Reference to Azure CosmosDb Sink * Don't warn on old file location * Update test names * Add syslog test * dotnet format * Add lazy syslog fix * Add longer wait for file * Make syslog test local only * Switch to shortened URL --- .github/renovate.json5 | 1 - src/Core/Utilities/LoggerFactoryExtensions.cs | 36 +++- .../Utilities/LoggerFactoryExtensionsTests.cs | 195 ++++++++++++++++++ 3 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 36f793e8c1..5c01832c06 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -84,7 +84,6 @@ "Serilog.AspNetCore", "Serilog.Extensions.Logging", "Serilog.Extensions.Logging.File", - "Serilog.Sinks.AzureCosmosDB", "Serilog.Sinks.SyslogMessages", "Stripe.net", "Swashbuckle.AspNetCore", diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index 5809da9c7a..54bd84df6f 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; using Bit.Core.Settings; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -33,7 +30,7 @@ public static class LoggerFactoryExtensions public static ILoggingBuilder AddSerilog( this ILoggingBuilder builder, WebHostBuilderContext context, - Func filter = null) + Func? filter = null) { var globalSettings = new GlobalSettings(); ConfigurationBinder.Bind(context.Configuration.GetSection("GlobalSettings"), globalSettings); @@ -57,19 +54,27 @@ public static class LoggerFactoryExtensions return filter(e, globalSettings); } + var logSentryWarning = false; + var logSyslogWarning = false; + + // Path format is the only required option for file logging, we will use that as + // the keystone for if they have configured the new location. + var newPathFormat = context.Configuration["Logging:PathFormat"]; + var config = new LoggerConfiguration() .MinimumLevel.Verbose() .Enrich.FromLogContext() .Filter.ByIncludingOnly(inclusionPredicate); - if (CoreHelpers.SettingHasValue(globalSettings?.Sentry.Dsn)) + if (CoreHelpers.SettingHasValue(globalSettings.Sentry.Dsn)) { config.WriteTo.Sentry(globalSettings.Sentry.Dsn) .Enrich.FromLogContext() .Enrich.WithProperty("Project", globalSettings.ProjectName); } - else if (CoreHelpers.SettingHasValue(globalSettings?.Syslog.Destination)) + else if (CoreHelpers.SettingHasValue(globalSettings.Syslog.Destination)) { + logSyslogWarning = true; // appending sitename to project name to allow easier identification in syslog. var appName = $"{globalSettings.SiteName}-{globalSettings.ProjectName}"; if (globalSettings.Syslog.Destination.Equals("local", StringComparison.OrdinalIgnoreCase)) @@ -107,10 +112,14 @@ public static class LoggerFactoryExtensions certProvider: new CertificateFileProvider(globalSettings.Syslog.CertificatePath, globalSettings.Syslog?.CertificatePassword ?? string.Empty)); } - } } } + else if (!string.IsNullOrEmpty(newPathFormat)) + { + // Use new location + builder.AddFile(context.Configuration.GetSection("Logging")); + } else if (CoreHelpers.SettingHasValue(globalSettings.LogDirectory)) { if (globalSettings.LogRollBySizeLimit.HasValue) @@ -138,6 +147,17 @@ public static class LoggerFactoryExtensions } var serilog = config.CreateLogger(); + + if (logSentryWarning) + { + serilog.Warning("Sentry for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation"); + } + + if (logSyslogWarning) + { + serilog.Warning("Syslog for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation"); + } + builder.AddSerilog(serilog); return builder; diff --git a/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs b/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs new file mode 100644 index 0000000000..06bb362336 --- /dev/null +++ b/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs @@ -0,0 +1,195 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Serilog; +using Serilog.Extensions.Logging; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class LoggerFactoryExtensionsTests +{ + [Fact] + public void AddSerilog_IsDevelopment_AddsNoProviders() + { + var providers = GetProviders([], "Development"); + + Assert.Empty(providers); + } + + [Fact] + public void AddSerilog_IsDevelopment_DevLoggingEnabled_AddsSerilog() + { + var providers = GetProviders(new Dictionary + { + { "GlobalSettings:EnableDevLogging", "true" }, + }, "Development"); + + var provider = Assert.Single(providers); + Assert.IsAssignableFrom(provider); + } + + [Fact] + public void AddSerilog_IsProduction_AddsSerilog() + { + var providers = GetProviders([]); + + var provider = Assert.Single(providers); + Assert.IsAssignableFrom(provider); + } + + [Fact] + public async Task AddSerilog_FileLogging_Old_Works() + { + var tempDir = Directory.CreateTempSubdirectory(); + + var providers = GetProviders(new Dictionary + { + { "GlobalSettings:ProjectName", "Test" }, + { "GlobalSetting:LogDirectoryByProject", "true" }, + { "GlobalSettings:LogDirectory", tempDir.FullName }, + }); + + var provider = Assert.Single(providers); + Assert.IsAssignableFrom(provider); + + var logger = provider.CreateLogger("Test"); + logger.LogWarning("This is a test"); + + var logFile = Assert.Single(tempDir.EnumerateFiles("Test/*.txt")); + + var logFileContents = await File.ReadAllTextAsync(logFile.FullName); + + Assert.Contains( + "This is a test", + logFileContents + ); + } + + [Fact] + public async Task AddSerilog_FileLogging_New_Works() + { + var tempDir = Directory.CreateTempSubdirectory(); + + var provider = GetServiceProvider(new Dictionary + { + { "Logging:PathFormat", $"{tempDir}/Logs/log-{{Date}}.log" }, + }, "Production"); + + var logger = provider + .GetRequiredService() + .CreateLogger("Test"); + + logger.LogWarning("This is a test"); + + // Writing to the file is buffered, give it a little time to flush + await Task.Delay(5); + + var logFile = Assert.Single(tempDir.EnumerateFiles("Logs/*.log")); + + var logFileContents = await File.ReadAllTextAsync(logFile.FullName); + + Assert.DoesNotContain( + "This configuration location for file logging has been deprecated.", + logFileContents + ); + Assert.Contains( + "This is a test", + logFileContents + ); + } + + [Fact(Skip = "Only for local development.")] + public async Task AddSerilog_SyslogConfigured_Warns() + { + // Setup a fake syslog server + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + using var listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 25000); + listener.Start(); + + var provider = GetServiceProvider(new Dictionary + { + { "GlobalSettings:SysLog:Destination", "tcp://127.0.0.1:25000" }, + { "GlobalSettings:SiteName", "TestSite" }, + { "GlobalSettings:ProjectName", "TestProject" }, + }, "Production"); + + var loggerFactory = provider.GetRequiredService(); + var logger = loggerFactory.CreateLogger("Test"); + + logger.LogWarning("This is a test"); + + // Look in syslog for data + using var socket = await listener.AcceptSocketAsync(cts.Token); + + // This is rather lazy as opposed to implementing smarter syslog message + // reading but thats not what this test about, so instead just give + // the sink time to finish its work in the background + + List messages = []; + + while (true) + { + var buffer = new byte[1024]; + var received = await socket.ReceiveAsync(buffer, SocketFlags.None, cts.Token); + + if (received == 0) + { + break; + } + + var response = Encoding.ASCII.GetString(buffer, 0, received); + messages.Add(response); + + if (messages.Count == 2) + { + break; + } + } + + Assert.Collection( + messages, + (firstMessage) => Assert.Contains("Syslog for logging has been deprecated", firstMessage), + (secondMessage) => Assert.Contains("This is a test", secondMessage) + ); + } + + private static IEnumerable GetProviders(Dictionary initialData, string environment = "Production") + { + var provider = GetServiceProvider(initialData, environment); + return provider.GetServices(); + } + + private static IServiceProvider GetServiceProvider(Dictionary initialData, string environment) + { + var config = new ConfigurationBuilder() + .AddInMemoryCollection(initialData) + .Build(); + + var hostingEnvironment = Substitute.For(); + + hostingEnvironment + .EnvironmentName + .Returns(environment); + + var context = new WebHostBuilderContext + { + HostingEnvironment = hostingEnvironment, + Configuration = config, + }; + + var services = new ServiceCollection(); + services.AddLogging(builder => + { + builder.AddSerilog(context); + }); + + return services.BuildServiceProvider(); + } +} From cff34b91941f8721f2cd6a26f24c38487508d9fb Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 28 Jul 2025 14:21:49 +0000 Subject: [PATCH 091/326] Bumped version to 2025.7.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index fb86d5f089..44df15a593 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.7.1 + 2025.7.2 Bit.$(MSBuildProjectName) enable From db4beb47f73ba3dca03ea3234fa7e63e369175b5 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:26:11 -0500 Subject: [PATCH 092/326] Enable disabled provider on successful update payment method invocation (#6129) --- .../VNext/ProviderBillingVNextController.cs | 14 ++++++++++++-- src/Core/Billing/Commands/BillingCommandResult.cs | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs index be7963236f..d0cc377245 100644 --- a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs @@ -1,8 +1,8 @@ -#nullable enable -using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; using Bit.Core.Utilities; @@ -19,6 +19,7 @@ public class ProviderBillingVNextController( IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, IGetPaymentMethodQuery getPaymentMethodQuery, + IProviderService providerService, IUpdateBillingAddressCommand updateBillingAddressCommand, IUpdatePaymentMethodCommand updatePaymentMethodCommand, IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController @@ -82,6 +83,15 @@ public class ProviderBillingVNextController( { var (paymentMethod, billingAddress) = request.ToDomain(); var result = await updatePaymentMethodCommand.Run(provider, paymentMethod, billingAddress); + // TODO: Temporary until we can send Provider notifications from the Billing API + if (!provider.Enabled) + { + await result.TapAsync(async _ => + { + provider.Enabled = true; + await providerService.UpdateAsync(provider); + }); + } return Handle(result); } diff --git a/src/Core/Billing/Commands/BillingCommandResult.cs b/src/Core/Billing/Commands/BillingCommandResult.cs index b69ad4bf12..3238ab4107 100644 --- a/src/Core/Billing/Commands/BillingCommandResult.cs +++ b/src/Core/Billing/Commands/BillingCommandResult.cs @@ -28,4 +28,10 @@ public class BillingCommandResult : OneOfBase(BadRequest badRequest) => new(badRequest); public static implicit operator BillingCommandResult(Conflict conflict) => new(conflict); public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); + + public Task TapAsync(Func f) => Match( + f, + _ => Task.CompletedTask, + _ => Task.CompletedTask, + _ => Task.CompletedTask); } From d407c164b6a5c02699abadaa65627a34e8486a27 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:56:20 -0400 Subject: [PATCH 093/326] BRE-917 Update to Alpine base (#5976) * testing-wolfi * testing alpine * fix gosu download * fix Admin dockerfile * update dockerfiles * alpine-compatible-entrypoint-script-for-api-test * make-entrypoint-scripts-alpine-compatible * testing nginx with alpine * cleaning up comments from dockerfile from testing * restore accidentally deleted icon * remove unused file * pin alpine, update apk add no cache * remove comments from testing * test shadow implementtaion for entrypoints * add shadow package, revert entrypoints, change from bash to shell for entry * add icu to setup container, update helpers to use shell * update migrator utility * add missing krb5 libraries --- bitwarden_license/src/Scim/Dockerfile | 22 ++++++------ bitwarden_license/src/Scim/entrypoint.sh | 2 +- bitwarden_license/src/Sso/Dockerfile | 22 ++++++------ bitwarden_license/src/Sso/entrypoint.sh | 2 +- src/Admin/Dockerfile | 45 +++++++++++++----------- src/Admin/entrypoint.sh | 2 +- src/Api/Dockerfile | 22 ++++++------ src/Api/entrypoint.sh | 2 +- src/Billing/Dockerfile | 20 +++++------ src/Billing/entrypoint.sh | 2 +- src/Events/Dockerfile | 22 ++++++------ src/Events/entrypoint.sh | 2 +- src/EventsProcessor/Dockerfile | 20 +++++------ src/EventsProcessor/entrypoint.sh | 2 +- src/Icons/Dockerfile | 21 +++++------ src/Icons/entrypoint.sh | 2 +- src/Identity/Dockerfile | 22 ++++++------ src/Identity/entrypoint.sh | 2 +- src/Notifications/Dockerfile | 20 +++++------ src/Notifications/entrypoint.sh | 2 +- util/Attachments/Dockerfile | 20 +++++------ util/Attachments/entrypoint.sh | 12 +++---- util/MsSqlMigratorUtility/Dockerfile | 13 ++++--- util/Nginx/Dockerfile | 10 +++--- util/Nginx/Dockerfile-k8s | 40 --------------------- util/Nginx/entrypoint.sh | 2 +- util/Nginx/setup-bwuser.sh | 11 +++--- util/Setup/Dockerfile | 20 +++++------ util/Setup/Helpers.cs | 2 +- util/Setup/entrypoint.sh | 2 +- 30 files changed, 176 insertions(+), 212 deletions(-) delete mode 100644 util/Nginx/Dockerfile-k8s diff --git a/bitwarden_license/src/Scim/Dockerfile b/bitwarden_license/src/Scim/Dockerfile index a0c5c88e49..fca3d83572 100644 --- a/bitwarden_license/src/Scim/Dockerfile +++ b/bitwarden_license/src/Scim/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + krb5 \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/bitwarden_license/src/Scim/entrypoint.sh b/bitwarden_license/src/Scim/entrypoint.sh index 41930504d3..b3cffa33bd 100644 --- a/bitwarden_license/src/Scim/entrypoint.sh +++ b/bitwarden_license/src/Scim/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/bitwarden_license/src/Sso/Dockerfile b/bitwarden_license/src/Sso/Dockerfile index d5d012b416..cbd049b9bd 100644 --- a/bitwarden_license/src/Sso/Dockerfile +++ b/bitwarden_license/src/Sso/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + krb5 \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/bitwarden_license/src/Sso/entrypoint.sh b/bitwarden_license/src/Sso/entrypoint.sh index c762659fb3..1d0f6d6a42 100644 --- a/bitwarden_license/src/Sso/entrypoint.sh +++ b/bitwarden_license/src/Sso/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile index 0d6fd4cc78..648ff1be91 100644 --- a/src/Admin/Dockerfile +++ b/src/Admin/Dockerfile @@ -1,40 +1,41 @@ +############################################### +# Node.js build stage # +############################################### +FROM node:20-alpine3.21 AS node-build + +WORKDIR /app +COPY src/Admin/package*.json ./ +COPY /src/Admin/ . +RUN npm ci +RUN npm run build + ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM # Determine proper runtime value for .NET RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt -# Set up Node -ARG NODE_VERSION=20 -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ - && apt-get update \ - && apt-get install -y nodejs \ - && npm install -g npm@latest && \ - rm -rf /var/lib/apt/lists/* - # Copy required project files WORKDIR /source COPY . ./ # Restore project dependencies and tools WORKDIR /source/src/Admin -RUN npm ci RUN . /tmp/rid.txt && dotnet restore -r $RID # Build project -RUN npm run build RUN . /tmp/rid.txt && dotnet publish \ -c release \ --no-restore \ @@ -46,25 +47,27 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + tzdata \ + krb5 \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app COPY --from=build /source/src/Admin/out /app +COPY --from=node-build /app/wwwroot /app/wwwroot COPY ./src/Admin/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 diff --git a/src/Admin/entrypoint.sh b/src/Admin/entrypoint.sh index 4d7d238d25..d003e4ec17 100644 --- a/src/Admin/entrypoint.sh +++ b/src/Admin/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile index 29adde878c..ef4c0c3ad8 100644 --- a/src/Api/Dockerfile +++ b/src/Api/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + krb5 \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Api/entrypoint.sh b/src/Api/entrypoint.sh index d89a4648ec..5e2addb503 100644 --- a/src/Api/entrypoint.sh +++ b/src/Api/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Billing/Dockerfile b/src/Billing/Dockerfile index 5eb4e9c0e0..ced8763577 100644 --- a/src/Billing/Dockerfile +++ b/src/Billing/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Billing/entrypoint.sh b/src/Billing/entrypoint.sh index 66540416f5..8b6a312ea1 100644 --- a/src/Billing/entrypoint.sh +++ b/src/Billing/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Events/Dockerfile b/src/Events/Dockerfile index 3a6342ef7a..913e94da45 100644 --- a/src/Events/Dockerfile +++ b/src/Events/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + krb5 \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Events/entrypoint.sh b/src/Events/entrypoint.sh index 92b19195ea..0497ceed60 100644 --- a/src/Events/entrypoint.sh +++ b/src/Events/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/EventsProcessor/Dockerfile b/src/EventsProcessor/Dockerfile index 928af7fb86..433552d321 100644 --- a/src/EventsProcessor/Dockerfile +++ b/src/EventsProcessor/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/EventsProcessor/entrypoint.sh b/src/EventsProcessor/entrypoint.sh index e0d2dc0230..f5757bc180 100644 --- a/src/EventsProcessor/entrypoint.sh +++ b/src/EventsProcessor/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Icons/Dockerfile b/src/Icons/Dockerfile index 16c88e22fa..5cd2b405d4 100644 --- a/src/Icons/Dockerfile +++ b/src/Icons/Dockerfile @@ -1,18 +1,18 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM # Determine proper runtime value for .NET RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -36,20 +36,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + krb5 \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Icons/entrypoint.sh b/src/Icons/entrypoint.sh index c65d3b308d..13bc1114aa 100644 --- a/src/Icons/entrypoint.sh +++ b/src/Icons/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Identity/Dockerfile b/src/Identity/Dockerfile index 9b9ae41334..41f23f6957 100644 --- a/src/Identity/Dockerfile +++ b/src/Identity/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,21 +37,21 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - krb5-user \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + krb5 \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Identity/entrypoint.sh b/src/Identity/entrypoint.sh index f5f84cc220..7141058c80 100644 --- a/src/Identity/entrypoint.sh +++ b/src/Identity/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/src/Notifications/Dockerfile b/src/Notifications/Dockerfile index 9cbc10e664..4aefaa9b90 100644 --- a/src/Notifications/Dockerfile +++ b/src/Notifications/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -37,20 +37,20 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/src/Notifications/entrypoint.sh b/src/Notifications/entrypoint.sh index d95324de2f..4c5759675b 100644 --- a/src/Notifications/entrypoint.sh +++ b/src/Notifications/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup diff --git a/util/Attachments/Dockerfile b/util/Attachments/Dockerfile index 24a315e99d..4ab1d0c11b 100644 --- a/util/Attachments/Dockerfile +++ b/util/Attachments/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -38,20 +38,20 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS=http://+:5000 ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /bitwarden_server diff --git a/util/Attachments/entrypoint.sh b/util/Attachments/entrypoint.sh index 1de574dc43..2c0942a148 100644 --- a/util/Attachments/entrypoint.sh +++ b/util/Attachments/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup @@ -23,11 +23,11 @@ if [ "$(id -u)" = "0" ] then # Create user and group - groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 || - groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1 - useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 || - usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 - mkhomedir_helper $USERNAME + addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true + adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true + mkdir -p /home/$USERNAME + chown $USERNAME:$GROUPNAME /home/$USERNAME + # The rest... diff --git a/util/MsSqlMigratorUtility/Dockerfile b/util/MsSqlMigratorUtility/Dockerfile index 990c25a7fb..b8bd7ff4a1 100644 --- a/util/MsSqlMigratorUtility/Dockerfile +++ b/util/MsSqlMigratorUtility/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -38,15 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 AS app ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false # Copy app from the build stage WORKDIR /app COPY --from=build /source/util/MsSqlMigratorUtility/out /app +RUN apk add --no-cache icu-libs + ENTRYPOINT ["sh", "-c", "/app/MsSqlMigratorUtility \"${MSSQL_CONN_STRING}\" ${@}", "--" ] diff --git a/util/Nginx/Dockerfile b/util/Nginx/Dockerfile index d0d05b0bf7..a497ccd17f 100644 --- a/util/Nginx/Dockerfile +++ b/util/Nginx/Dockerfile @@ -1,15 +1,13 @@ -FROM --platform=$BUILDPLATFORM nginx:stable +FROM --platform=$BUILDPLATFORM nginx:stable-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - gosu \ - curl \ - && rm -rf /var/lib/apt/lists/* +RUN apk add --no-cache curl \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu COPY util/Nginx/nginx.conf /etc/nginx COPY util/Nginx/proxy.conf /etc/nginx diff --git a/util/Nginx/Dockerfile-k8s b/util/Nginx/Dockerfile-k8s deleted file mode 100644 index 9f0d89ee1d..0000000000 --- a/util/Nginx/Dockerfile-k8s +++ /dev/null @@ -1,40 +0,0 @@ -FROM nginx:stable - -LABEL com.bitwarden.product="bitwarden" - -ENV USERNAME="bitwarden" -ENV GROUPNAME="bitwarden" - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - gosu \ - curl && \ - rm -rf /var/lib/apt/lists/* - -COPY nginx.conf /etc/nginx/nginx.conf -COPY proxy.conf /etc/nginx/proxy.conf -COPY mime.types /etc/nginx/mime.types -COPY security-headers.conf /etc/nginx/security-headers.conf -COPY security-headers-ssl.conf /etc/nginx/security-headers.conf - -COPY setup-bwuser.sh / - -EXPOSE 8000 - -EXPOSE 8080 -EXPOSE 8443 - -RUN chmod +x /setup-bwuser.sh - -RUN ./setup-bwuser.sh $USERNAME $GROUPNAME - -RUN mkdir -p /var/run/nginx && \ - touch /var/run/nginx/nginx.pid -RUN chown -R $USERNAME:$GROUPNAME /var/run/nginx && \ - chown -R $USERNAME:$GROUPNAME /var/cache/nginx && \ - chown -R $USERNAME:$GROUPNAME /var/log/nginx - - -HEALTHCHECK CMD curl --insecure -Lfs https://localhost:8443/alive || curl -Lfs http://localhost:8080/alive || exit 1 - -USER bitwarden diff --git a/util/Nginx/entrypoint.sh b/util/Nginx/entrypoint.sh index 0cf8a58888..0d4fa73802 100644 --- a/util/Nginx/entrypoint.sh +++ b/util/Nginx/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Setup diff --git a/util/Nginx/setup-bwuser.sh b/util/Nginx/setup-bwuser.sh index b17454722a..88e05a90a3 100644 --- a/util/Nginx/setup-bwuser.sh +++ b/util/Nginx/setup-bwuser.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Setup @@ -32,8 +32,7 @@ fi # Create user and group -groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 || -groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1 -useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 || -usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 -mkhomedir_helper $USERNAME +addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true +adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true +mkdir -p /home/$USERNAME +chown $USERNAME:$GROUPNAME /home/$USERNAME diff --git a/util/Setup/Dockerfile b/util/Setup/Dockerfile index b94c1f564c..fe1c8ea74b 100644 --- a/util/Setup/Dockerfile +++ b/util/Setup/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -9,11 +9,11 @@ ARG TARGETPLATFORM # Determine proper runtime value for .NET # We put the value in a file to be read by later layers. RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - RID=linux-x64 ; \ + RID=linux-musl-x64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - RID=linux-arm64 ; \ + RID=linux-musl-arm64 ; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ - RID=linux-arm ; \ + RID=linux-musl-arm ; \ fi \ && echo "RID=$RID" > /tmp/rid.txt @@ -38,18 +38,18 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" com.bitwarden.project="setup" - ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ +RUN apk add --no-cache curl \ openssl \ - gosu \ - && rm -rf /var/lib/apt/lists/* + icu-libs \ + shadow \ + && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage WORKDIR /app diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs index 07a8e0b1ef..1f091b674f 100644 --- a/util/Setup/Helpers.cs +++ b/util/Setup/Helpers.cs @@ -128,7 +128,7 @@ public static class Helpers if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var escapedArgs = cmd.Replace("\"", "\\\""); - process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.FileName = "/bin/sh"; process.StartInfo.Arguments = $"-c \"{escapedArgs}\""; } else diff --git a/util/Setup/entrypoint.sh b/util/Setup/entrypoint.sh index b981d760a9..417a6bb8a9 100644 --- a/util/Setup/entrypoint.sh +++ b/util/Setup/entrypoint.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh # Setup From 59e7bc7438d0a6da1fe900de74adba0c440d1dd2 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:34:42 +0200 Subject: [PATCH 094/326] Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response (#6093) --- .../Api/Response/UserDecryptionOptions.cs | 10 +- .../MasterPasswordUnlockResponseModel.cs | 20 ++++ .../UserDecryptionOptionsBuilder.cs | 47 +++++--- .../Endpoints/IdentityServerSsoTests.cs | 38 ++++++- .../Endpoints/IdentityServerTests.cs | 27 +++-- .../BaseRequestValidatorTests.cs | 102 ++++++++++++++++++ .../UserDecryptionOptionsBuilderTests.cs | 30 ++++++ 7 files changed, 247 insertions(+), 27 deletions(-) create mode 100644 src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs diff --git a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs index b5f2b77cfb..bd8542e8bf 100644 --- a/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs +++ b/src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs @@ -1,8 +1,7 @@ using System.Text.Json.Serialization; +using Bit.Core.KeyManagement.Models.Response; using Bit.Core.Models.Api; -#nullable enable - namespace Bit.Core.Auth.Models.Api.Response; public class UserDecryptionOptions : ResponseModel @@ -14,8 +13,15 @@ public class UserDecryptionOptions : ResponseModel /// /// Gets or sets whether the current user has a master password that can be used to decrypt their vault. /// + [Obsolete("Use MasterPasswordUnlock instead. This will be removed in a future version.")] public bool HasMasterPassword { get; set; } + /// + /// Gets or sets whether the current user has master password unlock data available. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; } + /// /// Gets or sets the WebAuthn PRF decryption keys. /// diff --git a/src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs b/src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs new file mode 100644 index 0000000000..f7d5dee852 --- /dev/null +++ b/src/Core/KeyManagement/Models/Response/MasterPasswordUnlockResponseModel.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.KeyManagement.Models.Response; + +public class MasterPasswordUnlockResponseModel +{ + public required MasterPasswordUnlockKdfResponseModel Kdf { get; init; } + [EncryptedString] public required string MasterKeyEncryptedUserKey { get; init; } + [StringLength(256)] public required string Salt { get; init; } +} + +public class MasterPasswordUnlockKdfResponseModel +{ + public required KdfType KdfType { get; init; } + public required int Iterations { get; init; } + public int? Memory { get; init; } + public int? Parallelism { get; init; } +} diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs index 61543f9751..dc27842210 100644 --- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs @@ -5,6 +5,7 @@ using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Response; using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.Identity.Utilities; @@ -25,7 +26,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private UserDecryptionOptions _options = new UserDecryptionOptions(); - private User? _user; + private User _user = null!; private SsoConfig? _ssoConfig; private Device? _device; @@ -44,7 +45,6 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder public IUserDecryptionOptionsBuilder ForUser(User user) { - _options.HasMasterPassword = user.HasMasterPassword(); _user = user; return this; } @@ -72,6 +72,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder public async Task BuildAsync() { + BuildMasterPasswordUnlock(); BuildKeyConnectorOptions(); await BuildTrustedDeviceOptions(); @@ -101,7 +102,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder } var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption }; - var isTdeOffboarding = _user != null && !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive; + var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive; if (!isTdeActive && !isTdeOffboarding) { return; @@ -116,7 +117,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder } var hasLoginApprovingDevice = false; - if (_device != null && _user != null) + if (_device != null) { var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id); // Checks if the current user has any devices that are capable of approving login with device requests except for @@ -134,16 +135,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId); } - var hasAdminApproval = false; - if (_user != null) - { - // If sso configuration data is not null then I know for sure that ssoConfiguration isn't null - var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id); + // If sso configuration data is not null then I know for sure that ssoConfiguration isn't null + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id); - hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin); - // They are only able to be approved by an admin if they have enrolled is reset password - hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); - } + hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin); + // They are only able to be approved by an admin if they have enrolled is reset password + var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); _options.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption( hasAdminApproval, @@ -153,4 +150,28 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder encryptedPrivateKey, encryptedUserKey); } + + private void BuildMasterPasswordUnlock() + { + if (_user.HasMasterPassword()) + { + _options.HasMasterPassword = true; + _options.MasterPasswordUnlock = new MasterPasswordUnlockResponseModel + { + Kdf = new MasterPasswordUnlockKdfResponseModel + { + KdfType = _user.Kdf, + Iterations = _user.KdfIterations, + Memory = _user.KdfMemory, + Parallelism = _user.KdfParallelism + }, + MasterKeyEncryptedUserKey = _user.Key!, + Salt = _user.Email.ToLowerInvariant() + }; + } + else + { + _options.HasMasterPassword = false; + } + } } diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index c4ceaa70df..b9ab1b0d02 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -36,7 +36,15 @@ public class IdentityServerSsoTests public async Task Test_MasterPassword_DecryptionType() { // Arrange - using var responseBody = await RunSuccessTestAsync(MemberDecryptionType.MasterPassword); + User? expectedUser = null; + using var responseBody = await RunSuccessTestAsync(async factory => + { + var database = factory.GetDatabaseContext(); + + expectedUser = await database.Users.SingleAsync(u => u.Email == TestEmail); + Assert.NotNull(expectedUser); + }, MemberDecryptionType.MasterPassword); + Assert.NotNull(expectedUser); // Assert // If the organization has a member decryption type of MasterPassword that should be the only option in the reply @@ -47,13 +55,33 @@ public class IdentityServerSsoTests // Expected to look like: // "UserDecryptionOptions": { // "Object": "userDecryptionOptions" - // "HasMasterPassword": true + // "HasMasterPassword": true, + // "MasterPasswordUnlock": { + // "Kdf": { + // "KdfType": 0, + // "Iterations": 600000 + // }, + // "MasterKeyEncryptedUserKey": "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==", + // "Salt": "sso_user@email.com" + // } // } AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True); - - // One property for the Object and one for master password - Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count()); + var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString(); + Assert.Equal("userDecryptionOptions", objectString); + var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object); + // MasterPasswordUnlock.Kdf + var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object); + var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32(); + Assert.Equal((int)expectedUser.Kdf, kdfType); + var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32(); + Assert.Equal(expectedUser.KdfIterations, kdfIterations); + // MasterPasswordUnlock.MasterKeyEncryptedUserKey + var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString(); + Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey); + // MasterPasswordUnlock.Salt + var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString(); + Assert.Equal(TestEmail, salt); } [Fact] diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index e63858117f..6f10f22002 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -67,7 +67,7 @@ public class IdentityServerTests : IClassFixture Assert.Equal(0, kdf); var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32(); Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, kdfIterations); - AssertUserDecryptionOptions(root); + AssertUserDecryptionOptions(root, user); } [Theory, RegisterFinishRequestModelCustomize] @@ -601,14 +601,27 @@ public class IdentityServerTests : IClassFixture Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant()); } - private static void AssertUserDecryptionOptions(JsonElement tokenResponse) + private static void AssertUserDecryptionOptions(JsonElement tokenResponse, User expectedUser) { - var userDecryptionOptions = AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object) - .EnumerateObject(); + var userDecryptionOptions = + AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object); - Assert.Collection(userDecryptionOptions, - (prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); }, - (prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); }); + AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True); + var objectString = AssertHelper.AssertJsonProperty(userDecryptionOptions, "Object", JsonValueKind.String).ToString(); + Assert.Equal("userDecryptionOptions", objectString); + var masterPasswordUnlock = AssertHelper.AssertJsonProperty(userDecryptionOptions, "MasterPasswordUnlock", JsonValueKind.Object); + // MasterPasswordUnlock.Kdf + var kdf = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Kdf", JsonValueKind.Object); + var kdfType = AssertHelper.AssertJsonProperty(kdf, "KdfType", JsonValueKind.Number).GetInt32(); + Assert.Equal((int)expectedUser.Kdf, kdfType); + var kdfIterations = AssertHelper.AssertJsonProperty(kdf, "Iterations", JsonValueKind.Number).GetInt32(); + Assert.Equal(expectedUser.KdfIterations, kdfIterations); + // MasterPasswordUnlock.MasterKeyEncryptedUserKey + var masterKeyEncryptedUserKey = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "MasterKeyEncryptedUserKey", JsonValueKind.String).ToString(); + Assert.Equal(expectedUser.Key, masterKeyEncryptedUserKey); + // MasterPasswordUnlock.Salt + var salt = AssertHelper.AssertJsonProperty(masterPasswordUnlock, "Salt", JsonValueKind.String).ToString(); + Assert.Equal(expectedUser.Email.ToLower(), salt); } private void ReinitializeDbForTests(IdentityApplicationFactory factory) diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index aab98a583c..a6283233dd 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -3,10 +3,13 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Api.Response; 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.Models.Api; using Bit.Core.Repositories; using Bit.Core.Services; @@ -27,6 +30,9 @@ namespace Bit.Identity.Test.IdentityServer; public class BaseRequestValidatorTests { + private static readonly string _mockEncryptedString = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + private UserManager _userManager; private readonly IUserService _userService; private readonly IEventService _eventService; @@ -377,6 +383,102 @@ public class BaseRequestValidatorTests Assert.Equal(expectedMessage, errorResponse.Message); } + [Theory, BitAutoData] + public async Task ValidateAsync_CustomResponse_NoMasterPassword_ShouldSetUserDecryptionOptions( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + _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 = false, + MasterPasswordUnlock = 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); + var customResponse = context.GrantResult.CustomResponse; + Assert.Contains("UserDecryptionOptions", customResponse); + Assert.IsType(customResponse["UserDecryptionOptions"]); + var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"]; + Assert.False(userDecryptionOptions.HasMasterPassword); + Assert.Null(userDecryptionOptions.MasterPasswordUnlock); + } + + [Theory] + [BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)] + [BitAutoData(KdfType.Argon2id, 11, 128, 5)] + public async Task ValidateAsync_CustomResponse_MasterPassword_ShouldSetUserDecryptionOptions( + KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + _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, + Iterations = kdfIterations, + Memory = kdfMemory, + Parallelism = kdfParallelism + }, + 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; + Assert.Contains("UserDecryptionOptions", customResponse); + Assert.IsType(customResponse["UserDecryptionOptions"]); + var userDecryptionOptions = (UserDecryptionOptions)customResponse["UserDecryptionOptions"]; + Assert.True(userDecryptionOptions.HasMasterPassword); + Assert.NotNull(userDecryptionOptions.MasterPasswordUnlock); + Assert.Equal(kdfType, userDecryptionOptions.MasterPasswordUnlock.Kdf.KdfType); + Assert.Equal(kdfIterations, userDecryptionOptions.MasterPasswordUnlock.Kdf.Iterations); + Assert.Equal(kdfMemory, userDecryptionOptions.MasterPasswordUnlock.Kdf.Memory); + Assert.Equal(kdfParallelism, userDecryptionOptions.MasterPasswordUnlock.Kdf.Parallelism); + Assert.Equal(_mockEncryptedString, userDecryptionOptions.MasterPasswordUnlock.MasterKeyEncryptedUserKey); + Assert.Equal("test@example.com", userDecryptionOptions.MasterPasswordUnlock.Salt); + } + private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index 25182743e5..b44dfe8d5f 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -28,6 +28,8 @@ public class UserDecryptionOptionsBuilderTests _organizationUserRepository = Substitute.For(); _loginApprovingClientTypes = Substitute.For(); _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes); + var user = new User(); + _builder.ForUser(user); } [Theory] @@ -285,4 +287,32 @@ public class UserDecryptionOptionsBuilderTests Assert.True(result.TrustedDeviceOption?.HasAdminApproval); } + + [Theory, BitAutoData] + public async Task Build_WhenUserHasNoMasterPassword_ShouldReturnNoMasterPasswordUnlock(User user) + { + user.MasterPassword = null; + + var result = await _builder.ForUser(user).BuildAsync(); + + Assert.False(result.HasMasterPassword); + Assert.Null(result.MasterPasswordUnlock); + } + + [Theory, BitAutoData] + public async Task Build_WhenUserHasMasterPassword_ShouldReturnMasterPasswordUnlock(User user) + { + user.Email = "test@example.COM"; + + var result = await _builder.ForUser(user).BuildAsync(); + + Assert.True(result.HasMasterPassword); + Assert.NotNull(result.MasterPasswordUnlock); + Assert.Equal(user.Kdf, result.MasterPasswordUnlock.Kdf.KdfType); + Assert.Equal(user.KdfIterations, result.MasterPasswordUnlock.Kdf.Iterations); + Assert.Equal(user.KdfMemory, result.MasterPasswordUnlock.Kdf.Memory); + Assert.Equal(user.KdfParallelism, result.MasterPasswordUnlock.Kdf.Parallelism); + Assert.Equal("test@example.com", result.MasterPasswordUnlock.Salt); + Assert.Equal(user.Key, result.MasterPasswordUnlock.MasterKeyEncryptedUserKey); + } } From abfb3a27b17119b41143ea467315e2b651826861 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:38:15 +0200 Subject: [PATCH 095/326] [PM-23242] Added UserDecryption with MasterPasswordUnlock as part of /sync response (#6102) * Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response * Added UserDecryption with MasterPasswordUnlock as part of /sync response --- .../Models/Response/DomainsResponseModel.cs | 8 +- .../Models/Response/SyncResponseModel.cs | 23 +++- .../Response/UserDecryptionResponseModel.cs | 9 ++ .../Vault/Controllers/SyncControllerTests.cs | 100 ++++++++++++++++++ .../Vault/Controllers/SyncControllerTests.cs | 49 +++++++++ 5 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs create mode 100644 test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs diff --git a/src/Api/Models/Response/DomainsResponseModel.cs b/src/Api/Models/Response/DomainsResponseModel.cs index 4df161f38e..82abddb4e4 100644 --- a/src/Api/Models/Response/DomainsResponseModel.cs +++ b/src/Api/Models/Response/DomainsResponseModel.cs @@ -8,10 +8,10 @@ using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; -public class DomainsResponseModel : ResponseModel +public class DomainsResponseModel() : ResponseModel("domains") { public DomainsResponseModel(User user, bool excluded = true) - : base("domains") + : this() { if (user == null) { @@ -38,13 +38,13 @@ public class DomainsResponseModel : ResponseModel public IEnumerable GlobalEquivalentDomains { get; set; } - public class GlobalDomains + public class GlobalDomains() { public GlobalDomains( GlobalEquivalentDomainsType globalDomain, IEnumerable domains, IEnumerable excludedDomains, - bool excluded) + bool excluded) : this() { Type = (byte)globalDomain; Domains = domains; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index dc34ffa46a..e19defce51 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -7,6 +7,7 @@ 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.Models.Api; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; @@ -18,7 +19,7 @@ using Bit.Core.Vault.Models.Data; namespace Bit.Api.Vault.Models.Response; -public class SyncResponseModel : ResponseModel +public class SyncResponseModel() : ResponseModel("sync") { public SyncResponseModel( GlobalSettings globalSettings, @@ -37,7 +38,7 @@ public class SyncResponseModel : ResponseModel bool excludeDomains, IEnumerable policies, IEnumerable sends) - : base("sync") + : this() { Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser); @@ -54,6 +55,23 @@ public class SyncResponseModel : ResponseModel Domains = excludeDomains ? null : new DomainsResponseModel(user, false); Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List(); Sends = sends.Select(s => new SendResponseModel(s, globalSettings)); + UserDecryption = new UserDecryptionResponseModel + { + MasterPasswordUnlock = user.HasMasterPassword() + ? new MasterPasswordUnlockResponseModel + { + Kdf = new MasterPasswordUnlockKdfResponseModel + { + KdfType = user.Kdf, + Iterations = user.KdfIterations, + Memory = user.KdfMemory, + Parallelism = user.KdfParallelism + }, + MasterKeyEncryptedUserKey = user.Key!, + Salt = user.Email.ToLowerInvariant() + } + : null + }; } public ProfileResponseModel Profile { get; set; } @@ -63,4 +81,5 @@ public class SyncResponseModel : ResponseModel public DomainsResponseModel Domains { get; set; } public IEnumerable Policies { get; set; } public IEnumerable Sends { get; set; } + public UserDecryptionResponseModel UserDecryption { get; set; } } diff --git a/src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs b/src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs new file mode 100644 index 0000000000..a4d259a00a --- /dev/null +++ b/src/Core/KeyManagement/Models/Response/UserDecryptionResponseModel.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.KeyManagement.Models.Response; + +public class UserDecryptionResponseModel +{ + /// + /// Returns the unlock data when the user has a master password that can be used to decrypt their vault. + /// + public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; } +} diff --git a/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs b/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs new file mode 100644 index 0000000000..16d4b0fb66 --- /dev/null +++ b/test/Api.IntegrationTest/Vault/Controllers/SyncControllerTests.cs @@ -0,0 +1,100 @@ +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.Vault.Models.Response; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.IntegrationTest.Vault.Controllers; + +public class SyncControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + + private readonly LoginHelper _loginHelper; + + private readonly IUserRepository _userRepository; + private string _ownerEmail = null!; + + public SyncControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + _userRepository = _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] + // [BitAutoData] + public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull() + { + var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(tempEmail); + await _loginHelper.LoginAsync(tempEmail); + + // Remove user's password. + var user = await _userRepository.GetByEmailAsync(tempEmail); + Assert.NotNull(user); + user.MasterPassword = null; + await _userRepository.UpsertAsync(user); + + var response = await _client.GetAsync("/sync"); + response.EnsureSuccessStatusCode(); + + var syncResponseModel = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(syncResponseModel); + Assert.NotNull(syncResponseModel.UserDecryption); + Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock); + } + + [Theory] + [BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)] + [BitAutoData(KdfType.Argon2id, 11, 128, 5)] + public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull( + KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(tempEmail); + await _loginHelper.LoginAsync(tempEmail); + + // Change KDF settings + var user = await _userRepository.GetByEmailAsync(tempEmail); + Assert.NotNull(user); + user.Kdf = kdfType; + user.KdfIterations = kdfIterations; + user.KdfMemory = kdfMemory; + user.KdfParallelism = kdfParallelism; + await _userRepository.UpsertAsync(user); + + var response = await _client.GetAsync("/sync"); + response.EnsureSuccessStatusCode(); + + var syncResponseModel = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(syncResponseModel); + Assert.NotNull(syncResponseModel.UserDecryption); + Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock); + Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf); + Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType); + Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations); + Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory); + Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism); + Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey); + Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt); + } +} diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index ebbfc2a2ba..54db1e4053 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -317,6 +317,55 @@ public class SyncControllerTests } } + [Theory] + [BitAutoData] + public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull( + User user, SutProvider sutProvider) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + user.MasterPassword = null; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + + var result = await sutProvider.Sut.Get(); + + Assert.Null(result.UserDecryption.MasterPasswordUnlock); + } + + [Theory] + [BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)] + [BitAutoData(KdfType.Argon2id, 11, 128, 5)] + public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull( + KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, + User user, SutProvider sutProvider) + { + user.EquivalentDomains = null; + user.ExcludedGlobalEquivalentDomains = null; + + user.Key = "test-key"; + user.MasterPassword = "test-master-password"; + user.Kdf = kdfType; + user.KdfIterations = kdfIterations; + user.KdfMemory = kdfMemory; + user.KdfParallelism = kdfParallelism; + + var userService = sutProvider.GetDependency(); + userService.GetUserByPrincipalAsync(Arg.Any()).ReturnsForAnyArgs(user); + + var result = await sutProvider.Sut.Get(); + + Assert.NotNull(result.UserDecryption.MasterPasswordUnlock); + Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf); + Assert.Equal(kdfType, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType); + Assert.Equal(kdfIterations, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations); + Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory); + Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism); + Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey); + Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt); + } private async Task AssertMethodsCalledAsync(IUserService userService, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, From df61bd5ccd9641e1108caf349cb547bd6bd58b17 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:25:44 +0200 Subject: [PATCH 096/326] [deps] Tools: Update aws-sdk-net monorepo (#6131) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 6f61b30bf4..79cd8bf9b8 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 3f508cd43bb912353e3ef698d0b4315f04bc0ce3 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Tue, 29 Jul 2025 05:58:17 -0400 Subject: [PATCH 097/326] add read actions (#6137) --- .github/workflows/build_target.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_target.yml b/.github/workflows/build_target.yml index 4b02ef2f4b..20c9cb8ef0 100644 --- a/.github/workflows/build_target.yml +++ b/.github/workflows/build_target.yml @@ -26,5 +26,6 @@ jobs: permissions: contents: read + actions: read id-token: write security-events: write From 52ef3ef7a5241fe77656c225313585200d5424b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:22:09 +0100 Subject: [PATCH 098/326] [PM-19195] Remove deprecated stored procedures (#6128) --- ...OrganizationUser_SetStatusForUsersById.sql | 29 ---------------- ...tRevisionDateByOrganizationUserIdsJson.sql | 33 ------------------- ...pJsonOrgUserSetStatusAndUserBumpSprocs.sql | 11 +++++++ 3 files changed, 11 insertions(+), 62 deletions(-) delete mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql delete mode 100644 src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql create mode 100644 util/Migrator/DbScripts/2025-07-28_00_DropJsonOrgUserSetStatusAndUserBumpSprocs.sql diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql deleted file mode 100644 index 18b876775e..0000000000 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_SetStatusForUsersById.sql +++ /dev/null @@ -1,29 +0,0 @@ -CREATE PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] - @OrganizationUserIds AS NVARCHAR(MAX), - @Status SMALLINT -AS -BEGIN - SET NOCOUNT ON - - -- Declare a table variable to hold the parsed JSON data - DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); - - -- Parse the JSON input into the table variable - INSERT INTO @ParsedIds (Id) - SELECT value - FROM OPENJSON(@OrganizationUserIds); - - -- Check if the input table is empty - IF (SELECT COUNT(1) FROM @ParsedIds) < 1 - BEGIN - RETURN(-1); - END - - UPDATE - [dbo].[OrganizationUser] - SET [Status] = @Status - WHERE [Id] IN (SELECT Id from @ParsedIds) - - EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] @OrganizationUserIds -END - diff --git a/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql b/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql deleted file mode 100644 index 6e4119d864..0000000000 --- a/src/Sql/dbo/Stored Procedures/User_BumpAccountRevisionDateByOrganizationUserIdsJson.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] - @OrganizationUserIds NVARCHAR(MAX) -AS -BEGIN - SET NOCOUNT ON - - CREATE TABLE #UserIds - ( - UserId UNIQUEIDENTIFIER NOT NULL - ); - - INSERT INTO #UserIds (UserId) - SELECT - OU.UserId - FROM - [dbo].[OrganizationUser] OU - INNER JOIN - (SELECT [value] as Id FROM OPENJSON(@OrganizationUserIds)) AS OUIds - ON OUIds.Id = OU.Id - WHERE - OU.[Status] = 2 -- Confirmed - - UPDATE - U - SET - U.[AccountRevisionDate] = GETUTCDATE() - FROM - [dbo].[User] U - INNER JOIN - #UserIds ON U.[Id] = #UserIds.[UserId] - - DROP TABLE #UserIds -END diff --git a/util/Migrator/DbScripts/2025-07-28_00_DropJsonOrgUserSetStatusAndUserBumpSprocs.sql b/util/Migrator/DbScripts/2025-07-28_00_DropJsonOrgUserSetStatusAndUserBumpSprocs.sql new file mode 100644 index 0000000000..3accd125ac --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-28_00_DropJsonOrgUserSetStatusAndUserBumpSprocs.sql @@ -0,0 +1,11 @@ +IF OBJECT_ID('[dbo].[OrganizationUser_SetStatusForUsersById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_SetStatusForUsersById] +END +GO + +IF OBJECT_ID('[dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[User_BumpAccountRevisionDateByOrganizationUserIdsJson] +END +GO From 6dea40c8686511ced2295e60463fc55c1d9d8e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:04:00 +0100 Subject: [PATCH 099/326] [PM-23987] Fix saving to default collections by updating collection lookup (#6122) * Refactor ICollectionRepository.GetManyByOrganizationIdAsync logic to include default user collections * Add stored procedure Collection_ReadSharedCollectionsByOrganizationId to retrieve collections by organization ID, excluding default user collections. * Add GetManySharedCollectionsByOrganizationIdAsync method to ICollectionRepository and its implementations to retrieve collections excluding default user collections. * Add unit test for GetManySharedCollectionsByOrganizationIdAsync method in CollectionRepositoryTests to verify retrieval of collections excluding default user collections. * Refactor controllers to use GetManySharedCollectionsByOrganizationIdAsync for retrieving shared collections * Update unit tests to use GetManySharedCollectionsByOrganizationIdAsync for verifying shared collections retrieval * Revert CiphersController.CanEditItemsInCollections to use GetManyByOrganizationIdAsync for retrieving organization collections * Update stored procedures to retrieve only DefaultUserCollection by modifying the WHERE clause in Collection_ReadSharedCollectionsByOrganizationId.sql and its corresponding migration script. * Update EF CollectionRepository.GetManySharedCollectionsByOrganizationIdAsync to filter collections by SharedCollection * Update OrganizationUserRepository.GetManyDetailsByOrganizationAsync_vNext to only include Shared collections * Update comments in stored procedure and migration script to clarify filtering for SharedCollections only --- src/Api/Controllers/CollectionsController.cs | 2 +- .../Controllers/CollectionsController.cs | 2 +- .../Repositories/ICollectionRepository.cs | 8 ++- .../Repositories/CollectionRepository.cs | 13 ++++ .../OrganizationUserRepository.cs | 16 +++-- .../Repositories/CollectionRepository.cs | 15 ++++- .../Collection_ReadByOrganizationId.sql | 3 +- ..._ReadSharedCollectionsByOrganizationId.sql | 14 ++++ ...serUserDetails_ReadByOrganizationId_V2.sql | 4 +- .../Controllers/CollectionsControllerTests.cs | 4 +- .../CollectionRepositoryTests.cs | 67 ++++++++++++++++++- .../OrganizationUserRepositoryTests.cs | 19 ++++++ .../2025-07-24_00_ReadSharedCollections.sql | 65 ++++++++++++++++++ 13 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-07-24_00_ReadSharedCollections.sql diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 30d007a57e..6708a66326 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -109,7 +109,7 @@ public class CollectionsController : Controller var readAllAuthorized = (await _authorizationService.AuthorizeAsync(User, CollectionOperations.ReadAll(orgId))).Succeeded; if (readAllAuthorized) { - orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId); + orgCollections = await _collectionRepository.GetManySharedCollectionsByOrganizationIdAsync(orgId); } else { diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index 836fe3a4f9..8615113906 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -65,7 +65,7 @@ public class CollectionsController : Controller [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] public async Task List() { - var collections = await _collectionRepository.GetManyByOrganizationIdAsync( + var collections = await _collectionRepository.GetManySharedCollectionsByOrganizationIdAsync( _currentContext.OrganizationId.Value); // TODO: Get all CollectionGroup associations for the organization and marry them up here for the response. var collectionResponses = collections.Select(c => new CollectionResponseModel(c, null)); diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index da4f6aa580..9e2f253c9f 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -16,10 +16,16 @@ public interface ICollectionRepository : IRepository /// /// Return all collections that belong to the organization. Does not include any permission details or group/user - /// access relationships. Excludes default collections (My Items collections). + /// access relationships. /// Task> GetManyByOrganizationIdAsync(Guid organizationId); + /// + /// + /// Excludes default collections (My Items collections) - used by Admin Console Collections tab. + /// + Task> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId); + /// /// Return all shared collections that belong to the organization. Includes group/user access relationships for each collection. /// diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index 2e2c90d399..6b71b57e3d 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -79,6 +79,19 @@ public class CollectionRepository : Repository, ICollectionRep } } + public async Task> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[{Table}_ReadSharedCollectionsByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index a6bbf8e6e0..9c5f2a39cd 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -441,13 +441,15 @@ public class OrganizationUserRepository : Repository(), Collections = includeCollections - ? ou.CollectionUsers.Select(cu => new CollectionAccessSelection - { - Id = cu.CollectionId, - ReadOnly = cu.ReadOnly, - HidePasswords = cu.HidePasswords, - Manage = cu.Manage - }).ToList() + ? ou.CollectionUsers + .Where(cu => cu.Collection.Type == CollectionType.SharedCollection) + .Select(cu => new CollectionAccessSelection + { + Id = cu.CollectionId, + ReadOnly = cu.ReadOnly, + HidePasswords = cu.HidePasswords, + Manage = cu.Manage + }).ToList() : new List() }; diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 0840a3f751..3169f86420 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -212,13 +212,26 @@ public class CollectionRepository : Repository> GetManyByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = from c in dbContext.Collections + where c.OrganizationId == organizationId + select c; + var collections = await query.ToArrayAsync(); + return collections; + } + } + + public async Task> GetManySharedCollectionsByOrganizationIdAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var query = from c in dbContext.Collections where c.OrganizationId == organizationId && - c.Type != CollectionType.DefaultUserCollection + c.Type == CollectionType.SharedCollection select c; var collections = await query.ToArrayAsync(); return collections; diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql index 6a7fefeb6b..0d317ebded 100644 --- a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql @@ -9,6 +9,5 @@ BEGIN FROM [dbo].[CollectionView] WHERE - [OrganizationId] = @OrganizationId AND - [Type] != 1 -- Exclude DefaultUserCollection + [OrganizationId] = @OrganizationId END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationId.sql new file mode 100644 index 0000000000..9f54a7e10e --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadSharedCollectionsByOrganizationId.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CollectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] = 0 -- SharedCollections only +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql index 6bf32089c2..d581b3aa2c 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationId_V2.sql @@ -26,6 +26,8 @@ BEGIN SELECT cu.* FROM [dbo].[CollectionUser] cu INNER JOIN [dbo].[OrganizationUser] ou ON cu.OrganizationUserId = ou.Id - WHERE ou.OrganizationId = @OrganizationId + INNER JOIN [dbo].[Collection] c ON cu.CollectionId = c.Id + WHERE ou.OrganizationId = @OrganizationId + AND c.Type = 0 -- SharedCollections only END END diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index bdcf6bc74e..99e329b500 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -173,12 +173,12 @@ public class CollectionsControllerTests .Returns(AuthorizationResult.Success()); sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organization.Id) + .GetManySharedCollectionsByOrganizationIdAsync(organization.Id) .Returns(collections); var response = await sutProvider.Sut.Get(organization.Id); - await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdAsync(organization.Id); + await sutProvider.GetDependency().Received(1).GetManySharedCollectionsByOrganizationIdAsync(organization.Id); Assert.Equal(collections.Count, response.Data.Count()); } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index 90596a23b1..79a96eeaeb 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -525,7 +525,7 @@ public class CollectionRepositoryTests var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, }; await collectionRepository.CreateAsync(collection3, null, null); - // Create a default user collection (should not be returned by this method) + // Create a default user collection var defaultCollection = new Collection { Name = "My Items", @@ -536,12 +536,73 @@ public class CollectionRepositoryTests var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); + Assert.NotNull(collections); + Assert.Equal(4, collections.Count); + Assert.All(collections, c => Assert.Equal(organization.Id, c.OrganizationId)); + + Assert.Contains(collections, c => c.Name == "Collection 1"); + Assert.Contains(collections, c => c.Name == "Collection 2"); + Assert.Contains(collections, c => c.Name == "Collection 3"); + Assert.Contains(collections, c => c.Name == "My Items"); + } + + /// + /// Test to ensure organization properly retrieves shared collections + /// + [DatabaseTheory, DatabaseData] + public async Task GetManySharedCollectionsByOrganizationIdAsync_Success( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IOrganizationUserRepository organizationUserRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection1, null, null); + + var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection2, null, null); + + var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection3, null, null); + + // Create a default user collection (should not be returned by this method) + var defaultCollection = new Collection + { + Name = "My Items", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }; + await collectionRepository.CreateAsync(defaultCollection, null, null); + + var collections = await collectionRepository.GetManySharedCollectionsByOrganizationIdAsync(organization.Id); + Assert.NotNull(collections); Assert.Equal(3, collections.Count); // Should only return the 3 shared collections, excluding the default user collection Assert.All(collections, c => Assert.Equal(organization.Id, c.OrganizationId)); - Assert.All(collections, c => Assert.NotEqual(CollectionType.DefaultUserCollection, c.Type)); - // Verify specific collections are returned Assert.Contains(collections, c => c.Name == "Collection 1"); Assert.Contains(collections, c => c.Name == "Collection 2"); Assert.Contains(collections, c => c.Name == "Collection 3"); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 8eec878794..febfca89e4 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -911,6 +911,17 @@ public class OrganizationUserRepositoryTests RevisionDate = requestTime }); + var defaultUserCollection = await collectionRepository.CreateAsync(new Collection + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "My Items", + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = user1.Email, + CreationDate = requestTime, + RevisionDate = requestTime + }); + // Create organization user with both groups and collections using CreateManyAsync var createOrgUserWithCollections = new List { @@ -940,6 +951,13 @@ public class OrganizationUserRepositoryTests ReadOnly = false, HidePasswords = true, Manage = true + }, + new CollectionAccessSelection + { + Id = defaultUserCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true } ], Groups = [group1.Id, group2.Id] @@ -969,6 +987,7 @@ public class OrganizationUserRepositoryTests Assert.Equal(2, user1Result.Collections.Count()); Assert.Contains(user1Result.Collections, c => c.Id == collection1.Id); Assert.Contains(user1Result.Collections, c => c.Id == collection2.Id); + Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id); } [DatabaseTheory, DatabaseData] diff --git a/util/Migrator/DbScripts/2025-07-24_00_ReadSharedCollections.sql b/util/Migrator/DbScripts/2025-07-24_00_ReadSharedCollections.sql new file mode 100644 index 0000000000..3324e94964 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-24_00_ReadSharedCollections.sql @@ -0,0 +1,65 @@ +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CollectionView] + WHERE + [OrganizationId] = @OrganizationId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CollectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] = 0 -- SharedCollections only +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2] + @OrganizationId UNIQUEIDENTIFIER, + @IncludeGroups BIT = 0, + @IncludeCollections BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + -- Result Set 1: User Details (always returned) + SELECT * + FROM [dbo].[OrganizationUserUserDetailsView] + WHERE OrganizationId = @OrganizationId + + -- Result Set 2: Group associations (if requested) + IF @IncludeGroups = 1 + BEGIN + SELECT gu.* + FROM [dbo].[GroupUser] gu + INNER JOIN [dbo].[OrganizationUser] ou ON gu.OrganizationUserId = ou.Id + WHERE ou.OrganizationId = @OrganizationId + END + + -- Result Set 3: Collection associations (if requested) + IF @IncludeCollections = 1 + BEGIN + SELECT cu.* + FROM [dbo].[CollectionUser] cu + INNER JOIN [dbo].[OrganizationUser] ou ON cu.OrganizationUserId = ou.Id + INNER JOIN [dbo].[Collection] c ON cu.CollectionId = c.Id + WHERE ou.OrganizationId = @OrganizationId + AND c.Type = 0 -- SharedCollections only + END +END +GO From b00e689ff652120873b2f70f11380f25b4ff0402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:04:45 +0100 Subject: [PATCH 100/326] [PM-22558] Update IOrganizationUserRepository.ReplaceAsync to preserve existing access to collections of the type DefaultUserCollection (#6037) * feat: exclude DefaultUserCollection from GetManyByOrganizationIdWithPermissionsAsync Updated EF implementation, SQL procedure, and unit test to verify that default user collections are filtered from results * Update the public CollectionsController.Get method to return a NotFoundResult for collections of type DefaultUserCollection. * Add unit tests for the public CollectionsController * Update ICollectionRepository.GetManyByOrganizationIdAsync to exclude results of the type DefaultUserCollection Modified the SQL stored procedure and the EF query to reflect this change and added a new integration test to ensure the functionality works as expected. * Refactor CollectionsController to remove unused IApplicationCacheService dependency * Update IOrganizationUserRepository.GetDetailsByIdWithCollectionsAsync to exclude DefaultUserCollections * Update IOrganizationUserRepository.GetManyDetailsByOrganizationAsync to exclude DefaultUserCollections * Undo change to GetByIdWithCollectionsAsync * Update integration test to verify exclusion of DefaultUserCollection in OrganizationUserRepository.GetDetailsByIdWithCollectionsAsync * Clarify documentation in ICollectionRepository to specify that GetManyByOrganizationIdWithAccessAsync returns only shared collections belonging to the organization. * Update IOrganizationUserRepository.ReplaceAsync to preserve existing access to collections of the type DefaultUserCollection --- .../OrganizationUserRepository.cs | 8 +- ...OrganizationUser_UpdateWithCollections.sql | 3 + .../OrganizationUserRepositoryTests.cs | 63 +++++++++++++ ...eserveDefaultCollectionsAccessOnUpdate.sql | 89 +++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-07-04_00_PreserveDefaultCollectionsAccessOnUpdate.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 9c5f2a39cd..33ffc453d5 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -516,9 +516,11 @@ public class OrganizationUserRepository : Repository cu.OrganizationUserId == obj.Id) - .ToListAsync(); + // Retrieve all collection assignments, excluding DefaultUserCollection + var existingCollectionUsers = await (from cu in dbContext.CollectionUsers + join c in dbContext.Collections on cu.CollectionId equals c.Id + where cu.OrganizationUserId == obj.Id && c.Type != CollectionType.DefaultUserCollection + select cu).ToListAsync(); foreach (var requestedCollection in requestedCollections) { diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql index 6486e002c3..e030958c3e 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_UpdateWithCollections.sql @@ -72,8 +72,11 @@ BEGIN CU FROM [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CU.[CollectionId] WHERE CU.[OrganizationUserId] = @Id + AND C.[Type] != 1 -- Don't delete default collections AND NOT EXISTS ( SELECT 1 diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index febfca89e4..130add6332 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -1166,4 +1166,67 @@ public class OrganizationUserRepositoryTests Assert.NotNull(responseModel); Assert.Empty(responseModel); } + + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + // Create a regular collection and a default collection + var regularCollection = await collectionRepository.CreateTestCollectionAsync(organization); + + // Manually create default collection since CreateTestCollectionAsync doesn't support type parameter + var defaultCollection = new Collection + { + OrganizationId = organization.Id, + Name = $"Default Collection {Guid.NewGuid()}", + Type = CollectionType.DefaultUserCollection + }; + await collectionRepository.CreateAsync(defaultCollection); + + var newCollection = await collectionRepository.CreateTestCollectionAsync(organization); + + // Set up initial collection access: user has access to both regular and default collections + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = regularCollection.Id, ReadOnly = false, HidePasswords = false, Manage = false }, + new CollectionAccessSelection { Id = defaultCollection.Id, ReadOnly = false, HidePasswords = false, Manage = true } + ]); + + // Verify initial state + var (_, initialCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.Equal(2, initialCollections.Count); + Assert.Contains(initialCollections, c => c.Id == regularCollection.Id); + Assert.Contains(initialCollections, c => c.Id == defaultCollection.Id); + + // Act: Update collection access with only the new collection + // This should preserve the default collection but remove the regular collection + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = newCollection.Id, ReadOnly = false, HidePasswords = false, Manage = true } + ]); + + // Assert + var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.NotNull(actualOrgUser); + Assert.Equal(2, actualCollections.Count); // Should have default collection + new collection + + // Default collection should be preserved + var preservedDefaultCollection = actualCollections.FirstOrDefault(c => c.Id == defaultCollection.Id); + Assert.NotNull(preservedDefaultCollection); + Assert.True(preservedDefaultCollection.Manage); // Original permissions preserved + + // New collection should be added + var addedNewCollection = actualCollections.FirstOrDefault(c => c.Id == newCollection.Id); + Assert.NotNull(addedNewCollection); + Assert.True(addedNewCollection.Manage); + + // Regular collection should be removed + Assert.DoesNotContain(actualCollections, c => c.Id == regularCollection.Id); + } } diff --git a/util/Migrator/DbScripts/2025-07-04_00_PreserveDefaultCollectionsAccessOnUpdate.sql b/util/Migrator/DbScripts/2025-07-04_00_PreserveDefaultCollectionsAccessOnUpdate.sql new file mode 100644 index 0000000000..952a96d402 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-04_00_PreserveDefaultCollectionsAccessOnUpdate.sql @@ -0,0 +1,89 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @Email NVARCHAR(256), + @Key VARCHAR(MAX), + @Status SMALLINT, + @Type TINYINT, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @Permissions NVARCHAR(MAX), + @ResetPasswordKey VARCHAR(MAX), + @Collections AS [dbo].[CollectionAccessSelectionType] READONLY, + @AccessSecretsManager BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager + -- Update + UPDATE + [Target] + SET + [Target].[ReadOnly] = [Source].[ReadOnly], + [Target].[HidePasswords] = [Source].[HidePasswords], + [Target].[Manage] = [Source].[Manage] + FROM + [dbo].[CollectionUser] AS [Target] + INNER JOIN + @Collections AS [Source] ON [Source].[Id] = [Target].[CollectionId] + WHERE + [Target].[OrganizationUserId] = @Id + AND ( + [Target].[ReadOnly] != [Source].[ReadOnly] + OR [Target].[HidePasswords] != [Source].[HidePasswords] + OR [Target].[Manage] != [Source].[Manage] + ) + + -- Insert + INSERT INTO [dbo].[CollectionUser] + ( + [CollectionId], + [OrganizationUserId], + [ReadOnly], + [HidePasswords], + [Manage] + ) + SELECT + [Source].[Id], + @Id, + [Source].[ReadOnly], + [Source].[HidePasswords], + [Source].[Manage] + FROM + @Collections AS [Source] + INNER JOIN + [dbo].[Collection] C ON C.[Id] = [Source].[Id] AND C.[OrganizationId] = @OrganizationId + WHERE + NOT EXISTS ( + SELECT + 1 + FROM + [dbo].[CollectionUser] + WHERE + [CollectionId] = [Source].[Id] + AND [OrganizationUserId] = @Id + ) + + -- Delete + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CU.[CollectionId] + WHERE + CU.[OrganizationUserId] = @Id + AND C.[Type] != 1 -- Don't delete default collections + AND NOT EXISTS ( + SELECT + 1 + FROM + @Collections + WHERE + [Id] = CU.[CollectionId] + ) +END +GO From 47237fa88fde07b13072c0d06d17fc85f682067e Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:41:03 -0400 Subject: [PATCH 101/326] add missing tzdata library (#6136) --- src/Api/Dockerfile | 1 + src/Billing/Dockerfile | 1 + src/Notifications/Dockerfile | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile index ef4c0c3ad8..5815d06769 100644 --- a/src/Api/Dockerfile +++ b/src/Api/Dockerfile @@ -50,6 +50,7 @@ EXPOSE 5000 RUN apk add --no-cache curl \ krb5 \ icu-libs \ + tzdata \ shadow \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu diff --git a/src/Billing/Dockerfile b/src/Billing/Dockerfile index ced8763577..8f1a217b0e 100644 --- a/src/Billing/Dockerfile +++ b/src/Billing/Dockerfile @@ -50,6 +50,7 @@ EXPOSE 5000 RUN apk add --no-cache curl \ icu-libs \ shadow \ + tzdata \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage diff --git a/src/Notifications/Dockerfile b/src/Notifications/Dockerfile index 4aefaa9b90..1b0b507606 100644 --- a/src/Notifications/Dockerfile +++ b/src/Notifications/Dockerfile @@ -50,6 +50,7 @@ EXPOSE 5000 RUN apk add --no-cache curl \ icu-libs \ shadow \ + tzdata \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu # Copy app from the build stage From 43372b71684cc9a209d038b834cf8aa933d4bd26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 29 Jul 2025 16:17:16 +0100 Subject: [PATCH 102/326] [PM-20010] Fix purge logic to skip claimed user check for organization vault (#6107) * Implement unit tests for PostPurge method in CiphersController to handle various scenarios * Refactor PostPurge method in CiphersController to use Guid for organizationId parameter and update related unit tests * Refactor PostPurge method in CiphersController to skip checking if user is claimed if its purging the org vault --- .../Vault/Controllers/CiphersController.cs | 20 ++- .../Controllers/CiphersControllerTests.cs | 121 +++++++++++++++++- 2 files changed, 129 insertions(+), 12 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 98ec78e9a0..761a5a3726 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1113,7 +1113,7 @@ public class CiphersController : Controller } [HttpPost("purge")] - public async Task PostPurge([FromBody] SecretVerificationRequestModel model, string organizationId = null) + public async Task PostPurge([FromBody] SecretVerificationRequestModel model, Guid? organizationId = null) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -1128,24 +1128,22 @@ public class CiphersController : Controller throw new BadRequestException(ModelState); } - // Check if the user is claimed by any organization. - if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) - { - throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); - } - - if (string.IsNullOrWhiteSpace(organizationId)) + if (organizationId == null) { + // Check if the user is claimed by any organization. + if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) + { + throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); + } await _cipherRepository.DeleteByUserIdAsync(user.Id); } else { - var orgId = new Guid(organizationId); - if (!await _currentContext.EditAnyCollection(orgId)) + if (!await _currentContext.EditAnyCollection(organizationId!.Value)) { throw new NotFoundException(); } - await _cipherService.PurgeAsync(orgId); + await _cipherService.PurgeAsync(organizationId!.Value); } } diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 2819fb8880..416b92f841 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1,5 +1,6 @@ using System.Security.Claims; using System.Text.Json; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Vault.Controllers; using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; @@ -1789,5 +1790,123 @@ public class CiphersControllerTests ); } -} + [Theory, BitAutoData] + public async Task PostPurge_WhenUserNotFound_ThrowsUnauthorizedAccessException( + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns((User)null); + await Assert.ThrowsAsync(() => sutProvider.Sut.PostPurge(model)); + } + + [Theory, BitAutoData] + public async Task PostPurge_WhenUserVerificationFails_ThrowsBadRequestException( + User user, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .VerifySecretAsync(user, model.Secret) + .Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PostPurge(model)); + } + + [Theory, BitAutoData] + public async Task PostPurge_UserPurge_WithClaimedUser_ThrowsBadRequestException( + User user, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .VerifySecretAsync(user, model.Secret) + .Returns(true); + sutProvider.GetDependency() + .IsClaimedByAnyOrganizationAsync(user.Id) + .Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PostPurge(model)); + } + + [Theory, BitAutoData] + public async Task PostPurge_UserPurge_WithUnclaimedUser_Successful( + User user, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .VerifySecretAsync(user, model.Secret) + .Returns(true); + sutProvider.GetDependency() + .IsClaimedByAnyOrganizationAsync(user.Id) + .Returns(false); + + await sutProvider.Sut.PostPurge(model); + + await sutProvider.GetDependency() + .Received(1) + .DeleteByUserIdAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task PostPurge_OrganizationPurge_WithEditAnyCollectionPermission_Successful( + User user, + SecretVerificationRequestModel model, + Guid organizationId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .VerifySecretAsync(user, model.Secret) + .Returns(true); + sutProvider.GetDependency() + .IsClaimedByAnyOrganizationAsync(user.Id) + .Returns(true); + sutProvider.GetDependency() + .EditAnyCollection(organizationId) + .Returns(true); + + await sutProvider.Sut.PostPurge(model, organizationId); + + await sutProvider.GetDependency() + .Received(1) + .PurgeAsync(organizationId); + } + + [Theory, BitAutoData] + public async Task PostPurge_OrganizationPurge_WithInsufficientPermissions_ThrowsNotFoundException( + User user, + Guid organizationId, + SecretVerificationRequestModel model, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(Arg.Any()) + .Returns(user); + sutProvider.GetDependency() + .VerifySecretAsync(user, model.Secret) + .Returns(true); + sutProvider.GetDependency() + .IsClaimedByAnyOrganizationAsync(user.Id) + .Returns(false); + sutProvider.GetDependency() + .EditAnyCollection(organizationId) + .Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.PostPurge(model, organizationId)); + } +} From a84e5554fb3c8f5e3618c32a7fc4b1df02a6b939 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:22:21 -0400 Subject: [PATCH 103/326] [PM-17562] Refactor event integration methods / declarations in ServiceCollectionExtensions (#6118) * [PM-17562] Refactor event integration methods / declarations in ServiceCollectionExtensions * Refactored ServiceCollectionExtensions to use TryAdd and still launch unique listeneer services * Updated unit tests to match new generic format for Listeners * Fix method spacing * Update README to reflect new integration setup in ServiceCollectionExtensions * Move interfaces to I prefix; fix typo in subscription * Fix reference to IIntegrationListenerConfiguration --- .../HecListenerConfiguration.cs | 38 ++ .../IEventListenerConfiguration.cs | 8 + .../IIntegrationListenerConfiguration.cs | 18 + .../ListenerConfiguration.cs | 28 ++ .../RepositoryListenerConfiguration.cs | 17 + .../SlackListenerConfiguration.cs | 38 ++ .../WebhookListenerConfiguration.cs | 38 ++ .../AzureServiceBusEventListenerService.cs | 15 +- ...ureServiceBusIntegrationListenerService.cs | 22 +- .../EventIntegrations/README.md | 47 ++- .../RabbitMqEventListenerService.cs | 11 +- .../RabbitMqIntegrationListenerService.cs | 23 +- src/Core/Settings/GlobalSettings.cs | 3 +- .../Utilities/ServiceCollectionExtensions.cs | 385 +++++++++--------- .../TestListenerConfiguration.cs | 16 + ...zureServiceBusEventListenerServiceTests.cs | 35 +- ...rviceBusIntegrationListenerServiceTests.cs | 31 +- .../RabbitMqEventListenerServiceTests.cs | 29 +- ...RabbitMqIntegrationListenerServiceTests.cs | 24 +- 19 files changed, 512 insertions(+), 314 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs create mode 100644 test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs new file mode 100644 index 0000000000..37a0d68beb --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecListenerConfiguration.cs @@ -0,0 +1,38 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class HecListenerConfiguration(GlobalSettings globalSettings) + : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration +{ + public IntegrationType IntegrationType + { + get => IntegrationType.Hec; + } + + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.HecEventsQueueName; + } + + public string IntegrationQueueName + { + get => _globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName; + } + + public string IntegrationRetryQueueName + { + get => _globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName; + } + + public string IntegrationSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs new file mode 100644 index 0000000000..7b2dd1343e --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IEventListenerConfiguration.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public interface IEventListenerConfiguration +{ + public string EventQueueName { get; } + public string EventSubscriptionName { get; } + public string EventTopicName { get; } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs new file mode 100644 index 0000000000..322a1cd952 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationListenerConfiguration.cs @@ -0,0 +1,18 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public interface IIntegrationListenerConfiguration : IEventListenerConfiguration +{ + public IntegrationType IntegrationType { get; } + public string IntegrationQueueName { get; } + public string IntegrationRetryQueueName { get; } + public string IntegrationSubscriptionName { get; } + public string IntegrationTopicName { get; } + public int MaxRetries { get; } + + public string RoutingKey + { + get => IntegrationType.ToRoutingKey(); + } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs new file mode 100644 index 0000000000..662bb8241e --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/ListenerConfiguration.cs @@ -0,0 +1,28 @@ +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public abstract class ListenerConfiguration +{ + protected GlobalSettings _globalSettings; + + public ListenerConfiguration(GlobalSettings globalSettings) + { + _globalSettings = globalSettings; + } + + public int MaxRetries + { + get => _globalSettings.EventLogging.MaxRetries; + } + + public string EventTopicName + { + get => _globalSettings.EventLogging.AzureServiceBus.EventTopicName; + } + + public string IntegrationTopicName + { + get => _globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; + } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs new file mode 100644 index 0000000000..118b3a17fe --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/RepositoryListenerConfiguration.cs @@ -0,0 +1,17 @@ +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class RepositoryListenerConfiguration(GlobalSettings globalSettings) + : ListenerConfiguration(globalSettings), IEventListenerConfiguration +{ + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs new file mode 100644 index 0000000000..7dd834f51e --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackListenerConfiguration.cs @@ -0,0 +1,38 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class SlackListenerConfiguration(GlobalSettings globalSettings) : + ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration +{ + public IntegrationType IntegrationType + { + get => IntegrationType.Slack; + } + + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.SlackEventsQueueName; + } + + public string IntegrationQueueName + { + get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName; + } + + public string IntegrationRetryQueueName + { + get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName; + } + + public string IntegrationSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs new file mode 100644 index 0000000000..9d5bf811c7 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookListenerConfiguration.cs @@ -0,0 +1,38 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class WebhookListenerConfiguration(GlobalSettings globalSettings) + : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration +{ + public IntegrationType IntegrationType + { + get => IntegrationType.Webhook; + } + + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName; + } + + public string IntegrationQueueName + { + get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName; + } + + public string IntegrationRetryQueueName + { + get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName; + } + + public string IntegrationSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs index ffa148fc08..a4b83b8806 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs @@ -2,27 +2,26 @@ using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.Settings; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; namespace Bit.Core.Services; -public class AzureServiceBusEventListenerService : EventLoggingListenerService +public class AzureServiceBusEventListenerService : EventLoggingListenerService + where TConfiguration : IEventListenerConfiguration { private readonly ServiceBusProcessor _processor; public AzureServiceBusEventListenerService( + TConfiguration configuration, IEventMessageHandler handler, IAzureServiceBusService serviceBusService, - string subscriptionName, - GlobalSettings globalSettings, - ILogger logger) : base(handler, logger) + ILogger> logger) : base(handler, logger) { _processor = serviceBusService.CreateProcessor( - globalSettings.EventLogging.AzureServiceBus.EventTopicName, - subscriptionName, + topicName: configuration.EventTopicName, + subscriptionName: configuration.EventSubscriptionName, new ServiceBusProcessorOptions()); - _logger = logger; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs index 55a39ec774..6db811efd9 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs @@ -1,32 +1,36 @@ #nullable enable using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Bit.Core.Services; -public class AzureServiceBusIntegrationListenerService : BackgroundService +public class AzureServiceBusIntegrationListenerService : BackgroundService + where TConfiguration : IIntegrationListenerConfiguration { private readonly int _maxRetries; private readonly IAzureServiceBusService _serviceBusService; private readonly IIntegrationHandler _handler; private readonly ServiceBusProcessor _processor; - private readonly ILogger _logger; + private readonly ILogger> _logger; - public AzureServiceBusIntegrationListenerService(IIntegrationHandler handler, - string topicName, - string subscriptionName, - int maxRetries, + public AzureServiceBusIntegrationListenerService( + TConfiguration configuration, + IIntegrationHandler handler, IAzureServiceBusService serviceBusService, - ILogger logger) + ILogger> logger) { _handler = handler; _logger = logger; - _maxRetries = maxRetries; + _maxRetries = configuration.MaxRetries; _serviceBusService = serviceBusService; - _processor = _serviceBusService.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = _serviceBusService.CreateProcessor( + topicName: configuration.IntegrationTopicName, + subscriptionName: configuration.IntegrationSubscriptionName, + options: new ServiceBusProcessorOptions()); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index f48dee8aad..83b59cdec1 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -399,35 +399,44 @@ These names added here are what must match the values provided in the secrets or in Global Settings. This must be in place (and the local ASB emulator restarted) before you can use any code locally that accesses ASB resources. +## ListenerConfiguration + +New integrations will need their own subclass of `ListenerConfiguration` which also conforms to +`IIntegrationListenerConfiguration`. This class provides a way of accessing the previously configured +RabbitMQ queues and ASB subscriptions by referring to the values created in `GlobalSettings`. This new +listener configuration will be used to type the listener and provide the means to access the necessary +configurations for the integration. + ## ServiceCollectionExtensions + In our `ServiceCollectionExtensions`, we pull all the above pieces together to start listeners on each message -tier with handlers to process the integration. There are a number of helper methods in here to make this simple -to add a new integration - one call per platform. +tier with handlers to process the integration. -Also note that if an integration needs a custom singleton / service defined, the add listeners method is a -good place to set that up. For instance, `SlackIntegrationHandler` needs a `SlackService`, so the singleton -declaration is right above the add integration method for slack. Same thing for webhooks when it comes to -defining a custom HttpClient by name. +The core method for all event integration setup is `AddEventIntegrationServices`. This method is called by +both of the add listeners methods, which ensures that we have one common place to set up cross-messaging-platform +dependencies and integrations. For instance, `SlackIntegrationHandler` needs a `SlackService`, so +`AddEventIntegrationServices` has a call to `AddSlackService`. Same thing for webhooks when it +comes to defining a custom HttpClient by name. + +1. In `AddEventIntegrationServices` create the listener configuration: -1. In `AddRabbitMqListeners` add the integration: ``` csharp - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.ExampleEventsQueueName, - globalSettings.EventLogging.RabbitMq.ExampleIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.ExampleIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.MaxRetries, - IntegrationType.Example); + var exampleConfiguration = new ExampleListenerConfiguration(globalSettings); ``` -2. In `AddAzureServiceBusListeners` add the integration: +2. Add the integration to both the RabbitMQ and ASB specific declarations: + ``` csharp -services.AddAzureServiceBusIntegration( - eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleEventSubscriptionName, - integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleIntegrationSubscriptionName, - integrationType: IntegrationType.Example, - globalSettings: globalSettings); + services.AddRabbitMqIntegration(exampleConfiguration); ``` +and + +``` csharp + services.AddAzureServiceBusIntegration(exampleConfiguration); +``` + + # Deploying a new integration ## RabbitMQ diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs index bc2329930d..09ce4ce767 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs @@ -1,13 +1,15 @@ #nullable enable using System.Text; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; namespace Bit.Core.Services; -public class RabbitMqEventListenerService : EventLoggingListenerService +public class RabbitMqEventListenerService : EventLoggingListenerService + where TConfiguration : IEventListenerConfiguration { private readonly Lazy> _lazyChannel; private readonly string _queueName; @@ -15,12 +17,11 @@ public class RabbitMqEventListenerService : EventLoggingListenerService public RabbitMqEventListenerService( IEventMessageHandler handler, - string queueName, + TConfiguration configuration, IRabbitMqService rabbitMqService, - ILogger logger) : base(handler, logger) + ILogger> logger) : base(handler, logger) { - _logger = logger; - _queueName = queueName; + _queueName = configuration.EventQueueName; _rabbitMqService = rabbitMqService; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index db6a7f9510..e8f368fbe5 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -10,7 +10,8 @@ using RabbitMQ.Client.Events; namespace Bit.Core.Services; -public class RabbitMqIntegrationListenerService : BackgroundService +public class RabbitMqIntegrationListenerService : BackgroundService + where TConfiguration : IIntegrationListenerConfiguration { private readonly int _maxRetries; private readonly string _queueName; @@ -19,26 +20,24 @@ public class RabbitMqIntegrationListenerService : BackgroundService private readonly IIntegrationHandler _handler; private readonly Lazy> _lazyChannel; private readonly IRabbitMqService _rabbitMqService; - private readonly ILogger _logger; + private readonly ILogger> _logger; private readonly TimeProvider _timeProvider; - public RabbitMqIntegrationListenerService(IIntegrationHandler handler, - string routingKey, - string queueName, - string retryQueueName, - int maxRetries, + public RabbitMqIntegrationListenerService( + IIntegrationHandler handler, + TConfiguration configuration, IRabbitMqService rabbitMqService, - ILogger logger, + ILogger> logger, TimeProvider timeProvider) { _handler = handler; - _routingKey = routingKey; - _retryQueueName = retryQueueName; - _queueName = queueName; + _maxRetries = configuration.MaxRetries; + _routingKey = configuration.RoutingKey; + _retryQueueName = configuration.IntegrationRetryQueueName; + _queueName = configuration.IntegrationQueueName; _rabbitMqService = rabbitMqService; _logger = logger; _timeProvider = timeProvider; - _maxRetries = maxRetries; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index e49ef4a7f2..2d9301e451 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -288,6 +288,7 @@ public class GlobalSettings : IGlobalSettings public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); public int IntegrationCacheRefreshIntervalMinutes { get; set; } = 10; + public int MaxRetries { get; set; } = 3; public class AzureServiceBusSettings { @@ -295,7 +296,6 @@ public class GlobalSettings : IGlobalSettings private string _eventTopicName; private string _integrationTopicName; - public int MaxRetries { get; set; } = 3; public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription"; public virtual string SlackEventSubscriptionName { get; set; } = "events-slack-subscription"; public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription"; @@ -331,7 +331,6 @@ public class GlobalSettings : IGlobalSettings private string _eventExchangeName; private string _integrationExchangeName; - public int MaxRetries { get; set; } = 3; public int RetryTiming { get; set; } = 30000; // 30s public bool UseDelayPlugin { get; set; } = false; public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 0c3f0cbca1..48d3304ab0 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -373,7 +373,6 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); } public static IdentityBuilder AddCustomIdentityServices( @@ -550,198 +549,57 @@ public static class ServiceCollectionExtensions { if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) { - services.AddKeyedSingleton("storage"); + services.TryAddKeyedSingleton("storage"); if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) { - services.AddSingleton(); - services.AddKeyedSingleton("broadcast"); + services.TryAddSingleton(); + services.TryAddKeyedSingleton("broadcast"); } else { - services.AddKeyedSingleton("broadcast"); + services.TryAddKeyedSingleton("broadcast"); } } else if (globalSettings.SelfHosted) { - services.AddKeyedSingleton("storage"); + services.TryAddKeyedSingleton("storage"); if (IsRabbitMqEnabled(globalSettings)) { - services.AddSingleton(); - services.AddKeyedSingleton("broadcast"); + services.TryAddSingleton(); + services.TryAddKeyedSingleton("broadcast"); } else { - services.AddKeyedSingleton("broadcast"); + services.TryAddKeyedSingleton("broadcast"); } } else { - services.AddKeyedSingleton("storage"); - services.AddKeyedSingleton("broadcast"); + services.TryAddKeyedSingleton("storage"); + services.TryAddKeyedSingleton("broadcast"); } - services.AddScoped(); - return services; - } - - private static IServiceCollection AddAzureServiceBusEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - handler: provider.GetRequiredService(), - serviceBusService: provider.GetRequiredService(), - subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName, - globalSettings: globalSettings, - logger: provider.GetRequiredService>() - ) - ); - - return services; - } - - private static IServiceCollection AddAzureServiceBusIntegration( - this IServiceCollection services, - string eventSubscriptionName, - string integrationSubscriptionName, - IntegrationType integrationType, - GlobalSettings globalSettings) - where TConfig : class - where THandler : class, IIntegrationHandler - { - var routingKey = integrationType.ToRoutingKey(); - - services.AddKeyedSingleton(routingKey, (provider, _) => - new EventIntegrationHandler( - integrationType, - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService>>())); - - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - handler: provider.GetRequiredKeyedService(routingKey), - serviceBusService: provider.GetRequiredService(), - subscriptionName: eventSubscriptionName, - globalSettings: globalSettings, - logger: provider.GetRequiredService>() - ) - ); - - services.AddSingleton, THandler>(); - services.AddSingleton(provider => - new AzureServiceBusIntegrationListenerService( - handler: provider.GetRequiredService>(), - topicName: globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName, - subscriptionName: integrationSubscriptionName, - maxRetries: globalSettings.EventLogging.AzureServiceBus.MaxRetries, - serviceBusService: provider.GetRequiredService(), - logger: provider.GetRequiredService>())); - + services.TryAddScoped(); return services; } public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) { - if (!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) || - !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) + if (!IsAzureServiceBusEnabled(globalSettings)) + { return services; + } - services.AddSingleton(); - services.AddSingleton(provider => - provider.GetRequiredService()); - services.AddHostedService(provider => provider.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddAzureServiceBusEventRepositoryListener(globalSettings); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddKeyedSingleton("persistent"); + services.TryAddSingleton(); - services.AddSlackService(globalSettings); - services.AddAzureServiceBusIntegration( - eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName, - integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName, - integrationType: IntegrationType.Slack, - globalSettings: globalSettings); - - services.TryAddSingleton(TimeProvider.System); - services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); - services.AddAzureServiceBusIntegration( - eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName, - integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName, - integrationType: IntegrationType.Webhook, - globalSettings: globalSettings); - - services.AddAzureServiceBusIntegration( - eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName, - integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName, - integrationType: IntegrationType.Hec, - globalSettings: globalSettings); - - return services; - } - - private static IServiceCollection AddRabbitMqEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) - { - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName, - provider.GetRequiredService(), - provider.GetRequiredService>())); - - return services; - } - - private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services, - string eventQueueName, - string integrationQueueName, - string integrationRetryQueueName, - int maxRetries, - IntegrationType integrationType) - where TConfig : class - where THandler : class, IIntegrationHandler - { - var routingKey = integrationType.ToRoutingKey(); - - services.AddKeyedSingleton(routingKey, (provider, _) => - new EventIntegrationHandler( - integrationType, - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService(), - provider.GetRequiredService>>())); - - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredKeyedService(routingKey), - eventQueueName, - provider.GetRequiredService(), - provider.GetRequiredService>())); - - services.AddSingleton, THandler>(); - services.AddSingleton(provider => - new RabbitMqIntegrationListenerService( - handler: provider.GetRequiredService>(), - routingKey: routingKey, - queueName: integrationQueueName, - retryQueueName: integrationRetryQueueName, - maxRetries: maxRetries, - rabbitMqService: provider.GetRequiredService(), - logger: provider.GetRequiredService>(), - timeProvider: provider.GetRequiredService())); + services.AddEventIntegrationServices(globalSettings); return services; } @@ -753,49 +611,15 @@ public static class ServiceCollectionExtensions return services; } - services.AddSingleton(); - services.AddSingleton(provider => - provider.GetRequiredService()); - services.AddHostedService(provider => provider.GetRequiredService()); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddRabbitMqEventRepositoryListener(globalSettings); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); - services.AddSlackService(globalSettings); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.MaxRetries, - IntegrationType.Slack); - - services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.MaxRetries, - IntegrationType.Webhook); - - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.HecEventsQueueName, - globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.MaxRetries, - IntegrationType.Hec); + services.AddEventIntegrationServices(globalSettings); return services; } - private static bool IsRabbitMqEnabled(GlobalSettings settings) - { - return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); - } - public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) { if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && @@ -803,11 +627,11 @@ public static class ServiceCollectionExtensions CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) { services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); + services.TryAddSingleton(); } else { - services.AddSingleton(); + services.TryAddSingleton(); } return services; @@ -1043,4 +867,161 @@ public static class ServiceCollectionExtensions return (provider, connectionString); } + + private static IServiceCollection AddAzureServiceBusIntegration(this IServiceCollection services, + TListenerConfig listenerConfiguration) + where TConfig : class + where TListenerConfig : IIntegrationListenerConfiguration + { + services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) => + new EventIntegrationHandler( + integrationType: listenerConfiguration.IntegrationType, + eventIntegrationPublisher: provider.GetRequiredService(), + integrationFilterService: provider.GetRequiredService(), + configurationCache: provider.GetRequiredService(), + userRepository: provider.GetRequiredService(), + organizationRepository: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ); + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new AzureServiceBusEventListenerService( + configuration: listenerConfiguration, + handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), + serviceBusService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ) + ); + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new AzureServiceBusIntegrationListenerService( + configuration: listenerConfiguration, + handler: provider.GetRequiredService>(), + serviceBusService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ) + ); + + return services; + } + + private static IServiceCollection AddEventIntegrationServices(this IServiceCollection services, + GlobalSettings globalSettings) + { + // Add common services + services.TryAddSingleton(); + services.TryAddSingleton(provider => + provider.GetRequiredService()); + services.AddHostedService(provider => provider.GetRequiredService()); + services.TryAddSingleton(); + services.TryAddKeyedSingleton("persistent"); + + // Add services in support of handlers + services.AddSlackService(globalSettings); + services.TryAddSingleton(TimeProvider.System); + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + + // Add integration handlers + services.TryAddSingleton, SlackIntegrationHandler>(); + services.TryAddSingleton, WebhookIntegrationHandler>(); + + var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings); + var slackConfiguration = new SlackListenerConfiguration(globalSettings); + var webhookConfiguration = new WebhookListenerConfiguration(globalSettings); + var hecConfiguration = new HecListenerConfiguration(globalSettings); + + if (IsRabbitMqEnabled(globalSettings)) + { + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new RabbitMqEventListenerService( + handler: provider.GetRequiredService(), + configuration: repositoryConfiguration, + rabbitMqService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ) + ); + services.AddRabbitMqIntegration(slackConfiguration); + services.AddRabbitMqIntegration(webhookConfiguration); + services.AddRabbitMqIntegration(hecConfiguration); + } + + if (IsAzureServiceBusEnabled(globalSettings)) + { + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new AzureServiceBusEventListenerService( + configuration: repositoryConfiguration, + handler: provider.GetRequiredService(), + serviceBusService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ) + ); + services.AddAzureServiceBusIntegration(slackConfiguration); + services.AddAzureServiceBusIntegration(webhookConfiguration); + services.AddAzureServiceBusIntegration(hecConfiguration); + } + + return services; + } + + private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services, + TListenerConfig listenerConfiguration) + where TConfig : class + where TListenerConfig : IIntegrationListenerConfiguration + { + services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) => + new EventIntegrationHandler( + integrationType: listenerConfiguration.IntegrationType, + eventIntegrationPublisher: provider.GetRequiredService(), + integrationFilterService: provider.GetRequiredService(), + configurationCache: provider.GetRequiredService(), + userRepository: provider.GetRequiredService(), + organizationRepository: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ); + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new RabbitMqEventListenerService( + handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), + configuration: listenerConfiguration, + rabbitMqService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>() + ) + ) + ); + services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider => + new RabbitMqIntegrationListenerService( + handler: provider.GetRequiredService>(), + configuration: listenerConfiguration, + rabbitMqService: provider.GetRequiredService(), + logger: provider.GetRequiredService>>(), + timeProvider: provider.GetRequiredService() + ) + ) + ); + + return services; + } + + private static bool IsAzureServiceBusEnabled(GlobalSettings settings) + { + return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName); + } + + private static bool IsRabbitMqEnabled(GlobalSettings settings) + { + return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); + } } diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs new file mode 100644 index 0000000000..b676cb44b9 --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/TestListenerConfiguration.cs @@ -0,0 +1,16 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class TestListenerConfiguration : IIntegrationListenerConfiguration +{ + public string EventQueueName => "event_queue"; + public string EventSubscriptionName => "event_subscription"; + public string EventTopicName => "event_topic"; + public IntegrationType IntegrationType => IntegrationType.Webhook; + public string IntegrationQueueName => "integration_queue"; + public string IntegrationRetryQueueName => "integration_retry_queue"; + public string IntegrationSubscriptionName => "integration_subscription"; + public string IntegrationTopicName => "integration_topic"; + public int MaxRetries => 3; +} diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs index 13704817ca..fb0adc2119 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Models.Data; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -14,20 +15,28 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class AzureServiceBusEventListenerServiceTests { - private readonly IEventMessageHandler _handler = Substitute.For(); - private readonly ILogger _logger = - Substitute.For>(); private const string _messageId = "messageId"; + private readonly TestListenerConfiguration _config = new(); - private SutProvider GetSutProvider() + private SutProvider> GetSutProvider() { - return new SutProvider() - .SetDependency(_handler) - .SetDependency(_logger) - .SetDependency("test-subscription", "subscriptionName") + return new SutProvider>() + .SetDependency(_config) .Create(); } + [Fact] + public void Constructor_CreatesProcessor() + { + var sutProvider = GetSutProvider(); + + sutProvider.GetDependency().Received(1).CreateProcessor( + Arg.Is(_config.EventTopicName), + Arg.Is(_config.EventSubscriptionName), + Arg.Any() + ); + } + [Theory, BitAutoData] public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) { @@ -35,7 +44,7 @@ public class AzureServiceBusEventListenerServiceTests await sutProvider.Sut.ProcessErrorAsync(args); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -49,7 +58,7 @@ public class AzureServiceBusEventListenerServiceTests var sutProvider = GetSutProvider(); await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -63,7 +72,7 @@ public class AzureServiceBusEventListenerServiceTests var sutProvider = GetSutProvider(); await sutProvider.Sut.ProcessReceivedMessageAsync("{ Inavlid JSON }", _messageId); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString().Contains("Invalid JSON")), @@ -80,7 +89,7 @@ public class AzureServiceBusEventListenerServiceTests _messageId ); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -97,7 +106,7 @@ public class AzureServiceBusEventListenerServiceTests _messageId ); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index 740baec37e..f450863ebf 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -14,33 +14,38 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class AzureServiceBusIntegrationListenerServiceTests { - private const int _maxRetries = 3; - private const string _topicName = "test_topic"; - private const string _subscriptionName = "test_subscription"; private readonly IIntegrationHandler _handler = Substitute.For(); private readonly IAzureServiceBusService _serviceBusService = Substitute.For(); - private readonly ILogger _logger = - Substitute.For>(); + private readonly TestListenerConfiguration _config = new(); - private SutProvider GetSutProvider() + private SutProvider> GetSutProvider() { - return new SutProvider() + return new SutProvider>() + .SetDependency(_config) .SetDependency(_handler) .SetDependency(_serviceBusService) - .SetDependency(_topicName, "topicName") - .SetDependency(_subscriptionName, "subscriptionName") - .SetDependency(_maxRetries, "maxRetries") - .SetDependency(_logger) .Create(); } + [Fact] + public void Constructor_CreatesProcessor() + { + var sutProvider = GetSutProvider(); + + sutProvider.GetDependency().Received(1).CreateProcessor( + Arg.Is(_config.IntegrationTopicName), + Arg.Is(_config.IntegrationSubscriptionName), + Arg.Any() + ); + } + [Theory, BitAutoData] public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) { var sutProvider = GetSutProvider(); await sutProvider.Sut.ProcessErrorAsync(args); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -70,7 +75,7 @@ public class AzureServiceBusIntegrationListenerServiceTests public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.RetryCount = _maxRetries; + message.RetryCount = _config.MaxRetries; var result = new IntegrationHandlerResult(false, message); result.Retryable = true; diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs index 8fd7e460be..cf1d8f6a0e 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Models.Data; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -15,16 +16,12 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class RabbitMqEventListenerServiceTests { - private const string _queueName = "test_queue"; - private readonly IRabbitMqService _rabbitMqService = Substitute.For(); - private readonly ILogger _logger = Substitute.For>(); + private readonly TestListenerConfiguration _config = new(); - private SutProvider GetSutProvider() + private SutProvider> GetSutProvider() { - return new SutProvider() - .SetDependency(_rabbitMqService) - .SetDependency(_logger) - .SetDependency(_queueName, "queueName") + return new SutProvider>() + .SetDependency(_config) .Create(); } @@ -35,8 +32,8 @@ public class RabbitMqEventListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - await _rabbitMqService.Received(1).CreateEventQueueAsync( - Arg.Is(_queueName), + await sutProvider.GetDependency().Received(1).CreateEventQueueAsync( + Arg.Is(_config.EventQueueName), Arg.Is(cancellationToken) ); } @@ -52,11 +49,11 @@ public class RabbitMqEventListenerServiceTests exchange: string.Empty, routingKey: string.Empty, new BasicProperties(), - body: new byte[0]); + body: Array.Empty()); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -75,11 +72,11 @@ public class RabbitMqEventListenerServiceTests exchange: string.Empty, routingKey: string.Empty, new BasicProperties(), - body: JsonSerializer.SerializeToUtf8Bytes("{ Inavlid JSON")); + body: JsonSerializer.SerializeToUtf8Bytes("{ Invalid JSON")); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Is(o => o.ToString().Contains("Invalid JSON")), @@ -102,7 +99,7 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -125,7 +122,7 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - _logger.Received(1).Log( + sutProvider.GetDependency>>().Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs index bb3f211afa..df40e17dd4 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -15,23 +15,17 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class RabbitMqIntegrationListenerServiceTests { - private const int _maxRetries = 3; - private const string _queueName = "test_queue"; - private const string _retryQueueName = "test_queue_retry"; - private const string _routingKey = "test_routing_key"; private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc); private readonly IIntegrationHandler _handler = Substitute.For(); private readonly IRabbitMqService _rabbitMqService = Substitute.For(); + private readonly TestListenerConfiguration _config = new(); - private SutProvider GetSutProvider() + private SutProvider> GetSutProvider() { - var sutProvider = new SutProvider() + var sutProvider = new SutProvider>() + .SetDependency(_config) .SetDependency(_handler) .SetDependency(_rabbitMqService) - .SetDependency(_queueName, "queueName") - .SetDependency(_retryQueueName, "retryQueueName") - .SetDependency(_routingKey, "routingKey") - .SetDependency(_maxRetries, "maxRetries") .WithFakeTimeProvider() .Create(); sutProvider.GetDependency().SetUtcNow(_now); @@ -46,10 +40,10 @@ public class RabbitMqIntegrationListenerServiceTests var cancellationToken = CancellationToken.None; await sutProvider.Sut.StartAsync(cancellationToken); - await _rabbitMqService.Received(1).CreateIntegrationQueuesAsync( - Arg.Is(_queueName), - Arg.Is(_retryQueueName), - Arg.Is(_routingKey), + await sutProvider.GetDependency().Received(1).CreateIntegrationQueuesAsync( + Arg.Is(_config.IntegrationQueueName), + Arg.Is(_config.IntegrationRetryQueueName), + Arg.Is(((IIntegrationListenerConfiguration)_config).RoutingKey), Arg.Is(cancellationToken) ); } @@ -101,7 +95,7 @@ public class RabbitMqIntegrationListenerServiceTests await sutProvider.Sut.StartAsync(cancellationToken); message.DelayUntilDate = null; - message.RetryCount = _maxRetries; + message.RetryCount = _config.MaxRetries; var eventArgs = new BasicDeliverEventArgs( consumerTag: string.Empty, deliveryTag: 0, From 2b0a639b95efcb5bdacaa740ce4a32ddf4b53fb7 Mon Sep 17 00:00:00 2001 From: Ruyut Date: Wed, 30 Jul 2025 17:28:51 +0800 Subject: [PATCH 104/326] fix: remove the duplicate name field (#6133) --- .github/ISSUE_TEMPLATE/bw-unified.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bw-unified.yml b/.github/ISSUE_TEMPLATE/bw-unified.yml index c1284f1839..240b1faa72 100644 --- a/.github/ISSUE_TEMPLATE/bw-unified.yml +++ b/.github/ISSUE_TEMPLATE/bw-unified.yml @@ -1,4 +1,3 @@ -name: Bitwarden Unified Bug Report name: Bitwarden Unified Deployment Bug Report description: File a bug report labels: [bug, bw-unified-deploy] From 5816ed6600445b0dd3288e6548ab2ac49f8577a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:00:48 +0100 Subject: [PATCH 105/326] =?UTF-8?q?[PM-23141]=C2=A0Fix:=20Users=20unable?= =?UTF-8?q?=20to=20edit=20ciphers=20after=20being=20confirmed=20into=20org?= =?UTF-8?q?anization=20(#6097)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor ConfirmOrganizationUserCommand to push registration after DB save * Assert device push registration handling in ConfirmOrganizationUserCommandTests --- .../ConfirmOrganizationUserCommand.cs | 15 +++++++++------ .../ConfirmOrganizationUserCommandTests.cs | 12 ++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 4ede530585..6ec69312ad 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -144,7 +144,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager); - await DeleteAndPushUserRegistrationAsync(organizationId, user.Id); succeededUsers.Add(orgUser); result.Add(Tuple.Create(orgUser, "")); } @@ -155,6 +154,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } await _organizationUserRepository.ReplaceManyAsync(succeededUsers); + await DeleteAndPushUserRegistrationAsync(organizationId, succeededUsers.Select(u => u.UserId!.Value)); return result; } @@ -208,12 +208,15 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } } - private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, IEnumerable userIds) { - var devices = await GetUserDeviceIdsAsync(userId); - await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, - organizationId.ToString()); - await _pushNotificationService.PushSyncOrgKeysAsync(userId); + foreach (var userId in userIds) + { + var devices = await GetUserDeviceIdsAsync(userId); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, + organizationId.ToString()); + await _pushNotificationService.PushSyncOrgKeysAsync(userId); + } } private async Task> GetUserDeviceIdsAsync(Guid userId) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 0bb38f7d0b..a6709cd10b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -12,6 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; @@ -118,6 +119,11 @@ public class ConfirmOrganizationUserCommandTests var organizationRepository = sutProvider.GetDependency(); var userRepository = sutProvider.GetDependency(); + var device = new Device() { Id = Guid.NewGuid(), UserId = user.Id, PushToken = "pushToken", Identifier = "identifier" }; + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id) + .Returns([device]); + org.PlanType = planType; orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; orgUser.UserId = user.Id; @@ -133,6 +139,12 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email); await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); + await sutProvider.GetDependency() + .Received(1) + .DeleteUserRegistrationOrganizationAsync( + Arg.Is>(ids => ids.Contains(device.Id.ToString()) && ids.Count() == 1), + org.Id.ToString()); + await sutProvider.GetDependency().Received(1).PushSyncOrgKeysAsync(user.Id); } From 531af410f9aeaa36b7d8089d5c69c69b132f658e Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:18:27 -0400 Subject: [PATCH 106/326] pm-24210 (#6142) --- src/Core/Auth/Entities/AuthRequest.cs | 23 +- .../Enums/DeviceValidationResultType.cs | 3 +- .../RequestValidators/DeviceValidator.cs | 30 +- .../ResourceOwnerPasswordValidator.cs | 34 +- .../ResourceOwnerPasswordValidatorTests.cs | 563 ++++++++++++++++-- .../IdentityServer/DeviceValidatorTests.cs | 10 +- 6 files changed, 577 insertions(+), 86 deletions(-) diff --git a/src/Core/Auth/Entities/AuthRequest.cs b/src/Core/Auth/Entities/AuthRequest.cs index af429adca2..2117c575c0 100644 --- a/src/Core/Auth/Entities/AuthRequest.cs +++ b/src/Core/Auth/Entities/AuthRequest.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Utilities; @@ -43,11 +44,31 @@ public class AuthRequest : ITableObject public bool IsSpent() { - return ResponseDate.HasValue || AuthenticationDate.HasValue || GetExpirationDate() < DateTime.UtcNow; + return ResponseDate.HasValue || AuthenticationDate.HasValue || IsExpired(); + } + + public bool IsExpired() + { + // TODO: PM-24252 - consider using TimeProvider for better mocking in tests + return GetExpirationDate() < DateTime.UtcNow; + } + + // TODO: PM-24252 - this probably belongs in a service. + public bool IsValidForAuthentication(Guid userId, + string password) + { + return ResponseDate.HasValue // it’s been responded to + && Approved == true // it was approved + && !IsExpired() // it's not expired + && Type == AuthRequestType.AuthenticateAndUnlock // it’s an authN request + && !AuthenticationDate.HasValue // it was not already used for authN + && UserId == userId // it belongs to the user + && CoreHelpers.FixedTimeEquals(AccessCode, password); // the access code matches the password } public DateTime GetExpirationDate() { + // TODO: PM-24252 - this should reference PasswordlessAuthSettings.UserRequestExpiration return CreationDate.AddMinutes(15); } } diff --git a/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs b/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs index 45c901e306..f8d04eccfb 100644 --- a/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs +++ b/src/Identity/IdentityServer/Enums/DeviceValidationResultType.cs @@ -6,5 +6,6 @@ public enum DeviceValidationResultType : byte InvalidUser = 1, InvalidNewDeviceOtp = 2, NewDeviceVerificationRequired = 3, - NoDeviceInformationProvided = 4 + NoDeviceInformationProvided = 4, + AuthRequestFlowUnknownDevice = 5, } diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 44dc89d259..42504ae813 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -39,6 +39,8 @@ public class DeviceValidator( private readonly ILogger _logger = logger; private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService; + private const string PasswordGrantType = "password"; + public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { // Parse device from request and return early if no device information is provided @@ -68,10 +70,14 @@ public class DeviceValidator( } // We have established that the device is unknown at this point; begin new device verification - if (request.GrantType == "password" && - request.Raw["AuthRequest"] == null && - !context.TwoFactorRequired && - !context.SsoRequired && + // for standard password grant type requests + // Note: the auth request flow re-uses the resource owner password flow but new device verification + // is not required for auth requests + var rawAuthRequestId = request.Raw["AuthRequest"]?.ToLowerInvariant(); + var isAuthRequest = !string.IsNullOrEmpty(rawAuthRequestId); + if (request.GrantType == PasswordGrantType && + !isAuthRequest && + context is { TwoFactorRequired: false, SsoRequired: false } && _globalSettings.EnableNewDeviceVerification) { var validationResult = await HandleNewDeviceVerificationAsync(context.User, request); @@ -87,6 +93,16 @@ public class DeviceValidator( } } + // Device still unknown, but if we are in an auth request flow, this is not valid + // as we only support auth request authN requests on known devices + if (request.GrantType == PasswordGrantType && isAuthRequest && + context is { TwoFactorRequired: false, SsoRequired: false }) + { + (context.ValidationErrorResult, context.CustomResponse) = + BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice); + return false; + } + // At this point we have established either new device verification is not required or the NewDeviceOtp is valid, // so we save the device to the database and proceed with authentication requestDevice.UserId = context.User.Id; @@ -252,7 +268,7 @@ public class DeviceValidator( var customResponse = new Dictionary(); switch (errorType) { - /* + /* * The ErrorMessage is brittle and is used to control the flow in the clients. Do not change them without updating the client as well. * There is a backwards compatibility issue as well: if you make a change on the clients then ensure that they are backwards * compatible. @@ -273,6 +289,10 @@ public class DeviceValidator( result.ErrorDescription = "No device information provided"; customResponse.Add("ErrorModel", new ErrorResponseModel("no device information provided")); break; + case DeviceValidationResultType.AuthRequestFlowUnknownDevice: + result.ErrorDescription = "Auth requests are not supported on unknown devices"; + customResponse.Add("ErrorModel", new ErrorResponseModel("auth request flow unsupported on unknown device")); + break; } return (result, customResponse); } diff --git a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index fe32d3e1b8..c5d2db38f4 100644 --- a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -11,7 +11,6 @@ using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; @@ -90,21 +89,30 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator { - private const string DefaultPassword = "master_password_hash"; - private const string DefaultUsername = "test@email.qa"; - private const string DefaultDeviceIdentifier = "test_identifier"; + private const string _defaultPassword = "master_password_hash"; + private const string _defaultUsername = "test@email.qa"; + private const string _defaultDeviceIdentifier = "test_identifier"; + private const DeviceType _defaultDeviceType = DeviceType.FirefoxBrowser; + private const string _defaultDeviceName = "firefox"; [Fact] public async Task ValidateAsync_Success() { // Arrange var localFactory = new IdentityApplicationFactory(); - await EnsureUserCreatedAsync(localFactory); + await RegisterUserAsync(localFactory); // Act var context = await localFactory.Server.PostAsync("/connect/token", @@ -74,12 +76,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); // Verify the User is not null to ensure the failure is due to bad password - Assert.NotNull(await userManager.FindByEmailAsync(DefaultUsername)); + Assert.NotNull(await userManager.FindByEmailAsync(_defaultUsername)); // Act var context = await localFactory.Server.PostAsync("/connect/token", @@ -95,23 +97,142 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); - var user = await userManager.FindByEmailAsync(DefaultUsername); + var user = await userManager.FindByEmailAsync(_defaultUsername); Assert.NotNull(user); - // Connect Request to User and set CreationDate - var authRequest = CreateAuthRequest( - user.Id, - AuthRequestType.AuthenticateAndUnlock, - DateTime.UtcNow.AddMinutes(-30) - ); + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create valid auth request and tie it to the user + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(_defaultDeviceType) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", _defaultDeviceName }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + var body = await AssertHelper.AssertResponseTypeIs(context); + var root = body.RootElement; + + var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString(); + Assert.NotNull(token); + } + + [Fact] + public async Task ValidateAsync_ValidateContextAsync_ValidAuthRequest_UnknownDevice_Failure() + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + + // Ensure User + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Create valid auth request and tie it to the user + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(_defaultDeviceType) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", _defaultDeviceName }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + var body = await AssertHelper.AssertResponseTypeIs(context); + var root = body.RootElement; + + var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object); + var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString(); + Assert.Equal("auth request flow unsupported on unknown device", errorMessage); + } + + [Fact] + public async Task ValidateAsync_ValidateContextAsync_Expired_AuthRequest_Failure() + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + // Ensure User + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create AuthRequest + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-10); // 10 minutes ago + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-16); // expired after 15 minutes + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + var authRequestRepository = localFactory.GetService(); await authRequestRepository.CreateAsync(authRequest); @@ -125,40 +246,51 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(context); - var root = body.RootElement; - var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString(); - Assert.NotNull(token); + await AssertStandardError(context); } [Fact] - public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeGreaterThanOneHour_Failure() + public async Task ValidateAsync_ValidateContextAsync_Unapproved_AuthRequest_Failure() { // Arrange var localFactory = new IdentityApplicationFactory(); // Ensure User - await EnsureUserCreatedAsync(localFactory); + await RegisterUserAsync(localFactory); var userManager = localFactory.GetService>(); - var user = await userManager.FindByEmailAsync(DefaultUsername); + var user = await userManager.FindByEmailAsync(_defaultUsername); Assert.NotNull(user); + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + // Create AuthRequest - var authRequest = CreateAuthRequest( - user.Id, - AuthRequestType.AuthenticateAndUnlock, - DateTime.UtcNow.AddMinutes(-61) - ); + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago + r.Approved = false; // NOT approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); // Act var context = await localFactory.Server.PostAsync("/connect/token", @@ -167,22 +299,137 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture InvalidAuthRequestTypes() + { + // yield the two enum values that should fail + yield return [AuthRequestType.Unlock]; + yield return [AuthRequestType.AdminApproval]; + } + + [Theory] + [MemberData(nameof(InvalidAuthRequestTypes))] + public async Task ValidateAsync_ValidateContextAsync_AuthRequest_Invalid_Type_Failure(AuthRequestType invalidType) + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + // Ensure User + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create AuthRequest + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid + r.Type = invalidType; // invalid type for authN + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", "firefox" }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + + await AssertStandardError(context); + } + + [Fact] + public async Task ValidateAsync_ValidateContextAsync_AuthRequest_WrongUser_Failure() + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + + // Ensure User 1 exists + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + + + // Ensure User 2 exists so we can satisfy auth request foreign key constraint + var user2Username = "user2@email.com"; + var user2Password = "user2_password"; + await RegisterUserAsync(localFactory, user2Username, user2Password); + var user2 = await userManager.FindByEmailAsync(user2Username); + Assert.NotNull(user2); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create valid auth request for user 2 + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // 2 minutes ago + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // not expired + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user2.Id; // connect request to user2 + r.AccessCode = _defaultPassword; // matches the password + }); + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user2.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(_defaultDeviceType) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", _defaultDeviceName }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert var body = await AssertHelper.AssertResponseTypeIs(context); var root = body.RootElement; @@ -191,17 +438,180 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); + + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create AuthRequest + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = null; // not answered + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // matches the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", "firefox" }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + + await AssertStandardError(context); + } + + [Fact] + public async Task ValidateAsync_ValidateContextAsync_WrongPassword_AuthRequest_Failure() + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + // Ensure User + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create AuthRequest + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // answered + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = null; // not used for authN yet + r.UserId = user.Id; // connect request to user + r.AccessCode = "WRONG_BAD_PASSWORD"; // does not match the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", "firefox" }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + + await AssertStandardError(context); + } + + [Fact] + public async Task ValidateAsync_ValidateContextAsync_Spent_AuthRequest_Failure() + { + // Arrange + var localFactory = new IdentityApplicationFactory(); + // Ensure User + await RegisterUserAsync(localFactory); + var userManager = localFactory.GetService>(); + + var user = await userManager.FindByEmailAsync(_defaultUsername); + Assert.NotNull(user); + + // Ensure device is known b/c auth requests aren't allowed for unknown devices. + await AddKnownDevice(localFactory, user.Id); + + // Create AuthRequest + var authRequest = CreateAuthRequest(r => + { + r.ResponseDate = DateTime.UtcNow.AddMinutes(-2); // answered + r.Approved = true; // approved + r.CreationDate = DateTime.UtcNow.AddMinutes(-5); // still valid + r.Type = AuthRequestType.AuthenticateAndUnlock; // authN request + r.AuthenticationDate = DateTime.UtcNow.AddMinutes(-2); // spent request - already has been used for authN + r.UserId = user.Id; // connect request to user + r.AccessCode = _defaultPassword; // does not match the password + }); + + var authRequestRepository = localFactory.GetService(); + await authRequestRepository.CreateAsync(authRequest); + + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); + Assert.NotEmpty(expectedAuthRequest); + + // Act + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "scope", "api offline_access" }, + { "client_id", "web" }, + { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, + { "deviceIdentifier", _defaultDeviceIdentifier }, + { "deviceName", "firefox" }, + { "grant_type", "password" }, + { "username", _defaultUsername }, + { "password", _defaultPassword }, + { "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() } + })); + + // Assert + await AssertStandardError(context); + } + + + + private async Task RegisterUserAsync( + IdentityApplicationFactory factory, + string username = _defaultUsername, + string password = _defaultPassword +) { - // Register user await factory.RegisterNewIdentityFactoryUserAsync( new RegisterFinishRequestModel { - Email = DefaultUsername, - MasterPasswordHash = DefaultPassword, + Email = username, + MasterPasswordHash = password, Kdf = KdfType.PBKDF2_SHA256, KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, - UserAsymmetricKeys = new KeysRequestModel() + UserAsymmetricKeys = new KeysRequestModel { PublicKey = "public_key", EncryptedPrivateKey = "private_key" @@ -218,11 +628,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture? customize = null) { - return new AuthRequest + var req = new AuthRequest { - UserId = userId, - Type = authRequestType, - Approved = approved, - RequestDeviceIdentifier = DefaultDeviceIdentifier, + // required fields with defaults + UserId = Guid.NewGuid(), + Type = AuthRequestType.AuthenticateAndUnlock, + RequestDeviceIdentifier = _defaultDeviceIdentifier, RequestIpAddress = "1.1.1.1", - AccessCode = DefaultPassword, + AccessCode = _defaultPassword, PublicKey = "test_public_key", - CreationDate = creationDate, - ResponseDate = responseDate, + CreationDate = DateTime.UtcNow, }; + + // let the caller tweak whatever they need + customize?.Invoke(req); + + return req; + } + + private async Task AddKnownDevice(IdentityApplicationFactory factory, Guid userId) + { + var userDevice = new Device + { + Identifier = _defaultDeviceIdentifier, + Type = _defaultDeviceType, + Name = _defaultDeviceName, + UserId = userId, + }; + var deviceRepository = factory.GetService(); + await deviceRepository.CreateAsync(userDevice); + } + + private async Task AssertStandardError(HttpContext context) + { + /* + An improvement on the current failure flow would be to document which part of + the flow failed since all of the failures are basically the same. + This doesn't build confidence in the tests. + */ + + var body = await AssertHelper.AssertResponseTypeIs(context); + var root = body.RootElement; + + var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object); + var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString(); + Assert.Equal("Username or password is incorrect. Try again.", errorMessage); } } diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 9058d26cf1..681b8c3a2f 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -325,12 +325,11 @@ public class DeviceValidatorTests } [Theory, BitAutoData] - public async void ValidateRequestDeviceAsync_IsAuthRequest_SavesDevice_ReturnsTrue( + public async void ValidateRequestDeviceAsync_IsAuthRequest_UnknownDevice_Errors( CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange - context.KnownDevice = false; ArrangeForHandleNewDeviceVerificationTest(context, request); AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) @@ -342,8 +341,11 @@ public class DeviceValidatorTests var result = await _sut.ValidateRequestDeviceAsync(request, context); // Assert - await _deviceService.Received(1).SaveAsync(context.Device); - Assert.True(result); + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + var expectedErrorMessage = "auth request flow unsupported on unknown device"; + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorMessage, actualResponse.Message); } [Theory, BitAutoData] From b5991776f40f136623b3781de192f08d19c9e0c3 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:49:48 -0400 Subject: [PATCH 107/326] pm-24208 (#6143) * pm-24208 --- .../src/Sso/Controllers/AccountController.cs | 43 ++++++++++++++++--- src/Core/Resources/SharedResources.en.resx | 6 +++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 00657a4e7f..5776912bd3 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -255,9 +255,12 @@ public class AccountController : Controller _logger.LogDebug("External claims: {@claims}", externalClaims); // Lookup our user and external provider info + // Note: the user will only exist if the user has already been provisioned and exists in the User table and the SSO user table. var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result); if (user == null) { + // User does not exist in SSO User table. They could have an existing BW account in the User table. + // This might be where you might initiate a custom workflow for user registration // in this sample we don't show how that would be done, as our sample implementation // simply auto-provisions new external user @@ -268,6 +271,8 @@ public class AccountController : Controller if (user != null) { + // User was JIT provisioned (this could be an existing user or a new user) + // This allows us to collect any additional claims or properties // for the specific protocols used and store them in the local auth cookie. // this is typically used to store data needed for signout from those protocols. @@ -487,12 +492,8 @@ public class AccountController : Controller throw new Exception(_i18nService.T("UserAlreadyExistsInviteProcess")); } - if (orgUser.Status == OrganizationUserStatusType.Invited) - { - // Org User is invited - they must manually accept the invite via email and authenticate with MP - // This allows us to enroll them in MP reset if required - throw new Exception(_i18nService.T("AcceptInviteBeforeUsingSSO", organization.DisplayName())); - } + EnsureOrgUserStatusAllowed(orgUser.Status, organization.DisplayName(), + allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]); // Accepted or Confirmed - create SSO link and return; await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser); @@ -587,6 +588,36 @@ public class AccountController : Controller return user; } + private void EnsureOrgUserStatusAllowed( + OrganizationUserStatusType status, + string organizationDisplayName, + params OrganizationUserStatusType[] allowedStatuses) + { + // if this status is one of the allowed ones, just return + if (allowedStatuses.Contains(status)) + { + return; + } + + // otherwise throw the appropriate exception + switch (status) + { + case OrganizationUserStatusType.Invited: + // Org User is invited – must accept via email first + throw new Exception( + _i18nService.T("AcceptInviteBeforeUsingSSO", organizationDisplayName)); + case OrganizationUserStatusType.Revoked: + // Revoked users may not be (auto)‑provisioned + throw new Exception( + _i18nService.T("OrganizationUserAccessRevoked", organizationDisplayName)); + default: + // anything else is “unknown” + throw new Exception( + _i18nService.T("OrganizationUserUnknownStatus", organizationDisplayName)); + } + } + + private IActionResult InvalidJson(string errorMessageKey, Exception ex = null) { Response.StatusCode = ex == null ? 400 : 500; diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 90a791222f..97cac5a610 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -532,6 +532,12 @@ To accept your invite to {0}, you must first log in using your master password. Once your invite has been accepted, you will be able to log in using SSO. + + Your access to organization {0} has been revoked. Please contact your administrator for assistance. + + + Your access to organization {0} is in an unknown state. Please contact your administrator for assistance. + You were removed from the organization managing single sign-on for your account. Contact the organization administrator for help regaining access to your account. From 64bf17684a0aecf461d0fb2f3a65d4c72581fd10 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:23:01 -0400 Subject: [PATCH 108/326] pm-24210-v2 (#6144) --- .../RequestValidators/DeviceValidator.cs | 3 +-- .../IdentityServer/DeviceValidatorTests.cs | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 42504ae813..80eb455519 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -95,8 +95,7 @@ public class DeviceValidator( // Device still unknown, but if we are in an auth request flow, this is not valid // as we only support auth request authN requests on known devices - if (request.GrantType == PasswordGrantType && isAuthRequest && - context is { TwoFactorRequired: false, SsoRequired: false }) + if (request.GrantType == PasswordGrantType && isAuthRequest) { (context.ValidationErrorResult, context.CustomResponse) = BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice); diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 681b8c3a2f..551f34b90a 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -324,13 +324,26 @@ public class DeviceValidatorTests Assert.True(result); } - [Theory, BitAutoData] + [Theory] + [BitAutoData(false, false)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + [BitAutoData(true, false)] + public async void ValidateRequestDeviceAsync_IsAuthRequest_UnknownDevice_Errors( + bool twoFactoRequired, bool ssoRequired, CustomValidatorRequestContext context, [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange - ArrangeForHandleNewDeviceVerificationTest(context, request); + request.GrantType = "password"; + context.TwoFactorRequired = twoFactoRequired; + context.SsoRequired = ssoRequired; + if (context.User != null) + { + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(365); + } + AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); From 574f7cba6785bbbcb32f4e8032db0ed5f48b4054 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:48:03 -0400 Subject: [PATCH 109/326] script syntax fix (#6146) --- bitwarden_license/src/Scim/entrypoint.sh | 8 ++++---- bitwarden_license/src/Sso/entrypoint.sh | 8 ++++---- src/Admin/entrypoint.sh | 4 ++-- src/Api/entrypoint.sh | 4 ++-- src/Events/entrypoint.sh | 4 ++-- src/Icons/entrypoint.sh | 4 ++-- src/Identity/entrypoint.sh | 8 ++++---- util/Setup/Dockerfile | 5 +++-- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/bitwarden_license/src/Scim/entrypoint.sh b/bitwarden_license/src/Scim/entrypoint.sh index b3cffa33bd..c3ff43e8dc 100644 --- a/bitwarden_license/src/Scim/entrypoint.sh +++ b/bitwarden_license/src/Scim/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,13 +46,13 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi -if [[ $globalSettings__selfHosted == "true" ]]; then - if [[ -z $globalSettings__identityServer__certificateLocation ]]; then +if [ "$globalSettings__selfHosted" = "true" ]; then + if [ -z "$globalSettings__identityServer__certificateLocation" ]; then export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx fi fi diff --git a/bitwarden_license/src/Sso/entrypoint.sh b/bitwarden_license/src/Sso/entrypoint.sh index 1d0f6d6a42..6ae590f18c 100644 --- a/bitwarden_license/src/Sso/entrypoint.sh +++ b/bitwarden_license/src/Sso/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,13 +46,13 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi -if [[ $globalSettings__selfHosted == "true" ]]; then - if [[ -z $globalSettings__identityServer__certificateLocation ]]; then +if [ "$globalSettings__selfHosted" = "true" ]; then + if [ -z "$globalSettings__identityServer__certificateLocation" ]; then export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx fi fi diff --git a/src/Admin/entrypoint.sh b/src/Admin/entrypoint.sh index d003e4ec17..21bb61716c 100644 --- a/src/Admin/entrypoint.sh +++ b/src/Admin/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,7 +46,7 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi diff --git a/src/Api/entrypoint.sh b/src/Api/entrypoint.sh index 5e2addb503..c4f31f1e5e 100644 --- a/src/Api/entrypoint.sh +++ b/src/Api/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,7 +46,7 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi diff --git a/src/Events/entrypoint.sh b/src/Events/entrypoint.sh index 0497ceed60..427bd06e40 100644 --- a/src/Events/entrypoint.sh +++ b/src/Events/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,7 +46,7 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi diff --git a/src/Icons/entrypoint.sh b/src/Icons/entrypoint.sh index 13bc1114aa..02408d1a68 100644 --- a/src/Icons/entrypoint.sh +++ b/src/Icons/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,7 +46,7 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi diff --git a/src/Identity/entrypoint.sh b/src/Identity/entrypoint.sh index 7141058c80..21f8556930 100644 --- a/src/Identity/entrypoint.sh +++ b/src/Identity/entrypoint.sh @@ -37,7 +37,7 @@ then mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden - if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then + if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then chown -R $USERNAME:$GROUPNAME /etc/bitwarden/kerberos fi @@ -46,13 +46,13 @@ else gosu_cmd="" fi -if [[ -f "/etc/bitwarden/kerberos/bitwarden.keytab" && -f "/etc/bitwarden/kerberos/krb5.conf" ]]; then +if [ -f "/etc/bitwarden/kerberos/bitwarden.keytab" ] && [ -f "/etc/bitwarden/kerberos/krb5.conf" ]; then cp -f /etc/bitwarden/kerberos/krb5.conf /etc/krb5.conf $gosu_cmd kinit $globalSettings__kerberosUser -k -t /etc/bitwarden/kerberos/bitwarden.keytab fi -if [[ $globalSettings__selfHosted == "true" ]]; then - if [[ -z $globalSettings__identityServer__certificateLocation ]]; then +if [ "$globalSettings__selfHosted" = "true" ]; then + if [ -z "$globalSettings__identityServer__certificateLocation" ]; then export globalSettings__identityServer__certificateLocation=/etc/bitwarden/identity/identity.pfx fi fi diff --git a/util/Setup/Dockerfile b/util/Setup/Dockerfile index fe1c8ea74b..80d00315e4 100644 --- a/util/Setup/Dockerfile +++ b/util/Setup/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build # Docker buildx supplies the value for this arg ARG TARGETPLATFORM @@ -38,7 +38,7 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" com.bitwarden.project="setup" @@ -48,6 +48,7 @@ ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false RUN apk add --no-cache curl \ openssl \ icu-libs \ + tzdata \ shadow \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu From 88463c1263f8357f87badec9b69d5b34fce8b869 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:26:33 -0400 Subject: [PATCH 110/326] pm-24210-v3 (#6148) --- .../CustomValidatorRequestContext.cs | 7 ++ .../RequestValidators/BaseRequestValidator.cs | 18 +++- .../CustomTokenRequestValidator.cs | 6 +- .../ResourceOwnerPasswordValidator.cs | 10 +- .../WebAuthnGrantValidator.cs | 6 +- .../BaseRequestValidatorTests.cs | 99 ++++++++++++++++++- .../BaseRequestValidatorTestWrapper.cs | 6 +- 7 files changed, 139 insertions(+), 13 deletions(-) diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index a53af41e66..a709a47cb2 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.Entities; using Bit.Core.Entities; using Duende.IdentityServer.Validation; @@ -41,4 +42,10 @@ public class CustomValidatorRequestContext /// This will be null if the authentication request is successful. /// public Dictionary CustomResponse { get; set; } + + /// + /// A validated auth request + /// + /// + public AuthRequest ValidatedAuthRequest { get; set; } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 0b33dabb77..3317e18264 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -35,6 +35,7 @@ public abstract class BaseRequestValidator where T : class private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; + private readonly IAuthRequestRepository _authRequestRepository; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -59,7 +60,9 @@ public abstract class BaseRequestValidator where T : class IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, - IPolicyRequirementQuery policyRequirementQuery) + IPolicyRequirementQuery policyRequirementQuery, + IAuthRequestRepository authRequestRepository + ) { _userManager = userManager; _userService = userService; @@ -76,6 +79,7 @@ public abstract class BaseRequestValidator where T : class SsoConfigRepository = ssoConfigRepository; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; PolicyRequirementQuery = policyRequirementQuery; + _authRequestRepository = authRequestRepository; } protected async Task ValidateAsync(T context, ValidatedTokenRequest request, @@ -190,6 +194,14 @@ public abstract class BaseRequestValidator where T : class return; } + // TODO: PM-24324 - This should be its own validator at some point. + // 6. Auth request handling + if (validatorContext.ValidatedAuthRequest != null) + { + validatorContext.ValidatedAuthRequest.AuthenticationDate = DateTime.UtcNow; + await _authRequestRepository.ReplaceAsync(validatorContext.ValidatedAuthRequest); + } + await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken); } @@ -404,8 +416,8 @@ public abstract class BaseRequestValidator where T : class /// /// Builds the custom response that will be sent to the client upon successful authentication, which /// includes the information needed for the client to initialize the user's account in state. - /// - /// The authenticated user. + /// + /// The authenticated user. /// The current request context. /// The device used for authentication. /// Whether to send a 2FA remember token. diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 5042f38b4f..c3d7908dc9 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -45,7 +45,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); _userDecryptionOptionsBuilder = Substitute.For(); _policyRequirementQuery = Substitute.For(); + _authRequestRepository = Substitute.For(); _sut = new BaseRequestValidatorTestWrapper( _userManager, @@ -84,7 +87,8 @@ public class BaseRequestValidatorTests _featureService, _ssoConfigRepository, _userDecryptionOptionsBuilder, - _policyRequirementQuery); + _policyRequirementQuery, + _authRequestRepository); } /* Logic path @@ -181,6 +185,99 @@ public class BaseRequestValidatorTests Assert.False(context.GrantResult.IsError); } + [Theory, BitAutoData] + public async Task ValidateAsync_ValidatedAuthRequest_ConsumedOnSuccess( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + // 1 -> to pass + _sut.isValid = true; + + var authRequest = new AuthRequest + { + Type = AuthRequestType.AuthenticateAndUnlock, + RequestDeviceIdentifier = "", + RequestIpAddress = "1.1.1.1", + AccessCode = "password", + PublicKey = "test_public_key", + CreationDate = DateTime.UtcNow.AddMinutes(-5), + ResponseDate = DateTime.UtcNow.AddMinutes(-2), + Approved = true, + AuthenticationDate = null, // unused + UserId = requestContext.User.Id, + }; + requestContext.ValidatedAuthRequest = authRequest; + + // 2 -> will result to false with no extra configuration + // 3 -> set two factor to be false + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + + // 4 -> set up device validator to pass + _deviceValidator.ValidateRequestDeviceAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + + // 5 -> not legacy user + _userService.IsLegacyUser(Arg.Any()) + .Returns(false); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + + // Check that the auth request was consumed + await _authRequestRepository.Received(1).ReplaceAsync(Arg.Is(ar => + ar.AuthenticationDate.HasValue)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_ValidatedAuthRequest_NotConsumed_When2faRequired( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + // 1 -> to pass + _sut.isValid = true; + + var authRequest = new AuthRequest + { + Type = AuthRequestType.AuthenticateAndUnlock, + RequestDeviceIdentifier = "", + RequestIpAddress = "1.1.1.1", + AccessCode = "password", + PublicKey = "test_public_key", + CreationDate = DateTime.UtcNow.AddMinutes(-5), + ResponseDate = DateTime.UtcNow.AddMinutes(-2), + Approved = true, + AuthenticationDate = null, // unused + UserId = requestContext.User.Id, + }; + requestContext.ValidatedAuthRequest = authRequest; + + // 2 -> will result to false with no extra configuration + // 3 -> set two factor to be required + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(true, null))); + + // Act + await _sut.ValidateAsync(context); + + // Assert we errored for 2fa requirement + Assert.True(context.GrantResult.IsError); + + // Assert that the auth request was NOT consumed + await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any()); + } + // Test grantTypes that require SSO when a user is in an organization that requires it [Theory] [BitAutoData("password")] diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 4c14de2d73..140e171309 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -62,7 +62,8 @@ IBaseRequestValidatorTestWrapper IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, - IPolicyRequirementQuery policyRequirementQuery) : + IPolicyRequirementQuery policyRequirementQuery, + IAuthRequestRepository authRequestRepository) : base( userManager, userService, @@ -78,7 +79,8 @@ IBaseRequestValidatorTestWrapper featureService, ssoConfigRepository, userDecryptionOptionsBuilder, - policyRequirementQuery) + policyRequirementQuery, + authRequestRepository) { } From cfcb24bbc94ff5bf746718ed2f857b0302fb0ca8 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:17:33 +1000 Subject: [PATCH 111/326] Update swagger description (#6140) --- src/Api/Utilities/ServiceCollectionExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index e6a20fe364..4f123d3f4f 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -33,7 +33,12 @@ public static class ServiceCollectionExtensions Url = new Uri("https://bitwarden.com"), Email = "support@bitwarden.com" }, - Description = "The Bitwarden public APIs.", + Description = """ + This schema documents the endpoints available to the Public API, which provides + organizations tools for managing members, collections, groups, event logs, and policies. + If you are looking for the Vault Management API, refer instead to + [this document](https://bitwarden.com/help/vault-management-api/). + """, License = new OpenApiLicense { Name = "GNU Affero General Public License v3.0", From 88dd9778482dcbc5cf69319ffe1fed1e866009f1 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:22:06 +1000 Subject: [PATCH 112/326] [PM-23921] [BEEEP] Add IOrganizationRequirements for each permission (#6105) * Add BasePermissionRequirement and implement it for each permission * Add tests --- .../Requirements/BasePermissionRequirement.cs | 24 +++++ .../ManageAccountRecoveryRequirement.cs | 20 ----- .../Requirements/ManageUsersRequirement.cs | 20 ----- .../Requirements/PermissionRequirements.cs | 11 +++ .../BasePermissionRequirementTests.cs | 66 ++++++++++++++ .../PermissionRequirementsTests.cs | 88 +++++++++++++++++++ .../Helpers/PermissionsHelpers.cs | 17 ++++ 7 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs delete mode 100644 src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs delete mode 100644 src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs create mode 100644 src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs create mode 100644 test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs create mode 100644 test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs new file mode 100644 index 0000000000..e904080043 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs @@ -0,0 +1,24 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +/// +/// A base implementation of which will authorize Owners, Admins, Providers, +/// and custom users with the permission specified by the permissionPicker constructor parameter. This is suitable +/// for most requirements related to a custom permission. +/// +/// A function that returns a custom permission which will authorize the action. +public abstract class BasePermissionRequirement(Func permissionPicker) : IOrganizationRequirement +{ + public async Task AuthorizeAsync(CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Type: OrganizationUserType.Custom } when permissionPicker(organizationClaims.Permissions) => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs deleted file mode 100644 index 268fee5d95..0000000000 --- a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable enable - -using Bit.Core.Context; -using Bit.Core.Enums; - -namespace Bit.Api.AdminConsole.Authorization.Requirements; - -public class ManageAccountRecoveryRequirement : IOrganizationRequirement -{ - public async Task AuthorizeAsync( - CurrentContextOrganization? organizationClaims, - Func> isProviderUserForOrg) - => organizationClaims switch - { - { Type: OrganizationUserType.Owner } => true, - { Type: OrganizationUserType.Admin } => true, - { Permissions.ManageResetPassword: true } => true, - _ => await isProviderUserForOrg() - }; -} diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs deleted file mode 100644 index 84f38e36c2..0000000000 --- a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable enable - -using Bit.Core.Context; -using Bit.Core.Enums; - -namespace Bit.Api.AdminConsole.Authorization.Requirements; - -public class ManageUsersRequirement : IOrganizationRequirement -{ - public async Task AuthorizeAsync( - CurrentContextOrganization? organizationClaims, - Func> isProviderUserForOrg) - => organizationClaims switch - { - { Type: OrganizationUserType.Owner } => true, - { Type: OrganizationUserType.Admin } => true, - { Permissions.ManageUsers: true } => true, - _ => await isProviderUserForOrg() - }; -} diff --git a/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs b/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs new file mode 100644 index 0000000000..e3100aff11 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs @@ -0,0 +1,11 @@ +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class AccessEventLogsRequirement() : BasePermissionRequirement(p => p.AccessEventLogs); +public class AccessImportExportRequirement() : BasePermissionRequirement(p => p.AccessImportExport); +public class AccessReportsRequirement() : BasePermissionRequirement(p => p.AccessReports); +public class ManageAccountRecoveryRequirement() : BasePermissionRequirement(p => p.ManageResetPassword); +public class ManageGroupsRequirement() : BasePermissionRequirement(p => p.ManageGroups); +public class ManagePoliciesRequirement() : BasePermissionRequirement(p => p.ManagePolicies); +public class ManageScimRequirement() : BasePermissionRequirement(p => p.ManageScim); +public class ManageSsoRequirement() : BasePermissionRequirement(p => p.ManageSso); +public class ManageUsersRequirement() : BasePermissionRequirement(p => p.ManageUsers); diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs new file mode 100644 index 0000000000..07d263b263 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs @@ -0,0 +1,66 @@ +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Test.AdminConsole.Helpers; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Requirements; + +public class BasePermissionRequirementTests +{ + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)] + public async Task Authorizes_Owners(CurrentContextOrganization organizationClaims) + { + var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)] + public async Task Authorizes_Admins(CurrentContextOrganization organizationClaims) + { + var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task Authorizes_Providers(CurrentContextOrganization organizationClaims) + { + var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(true)); + Assert.True(result); + } + + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task Authorizes_CustomPermission(CurrentContextOrganization organizationClaims) + { + organizationClaims.Permissions.ManageGroups = true; + var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task DoesNotAuthorize_Users(CurrentContextOrganization organizationClaims) + { + var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false)); + Assert.False(result); + } + + [Theory, BitAutoData] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task DoesNotAuthorize_OtherCustomPermissions(CurrentContextOrganization organizationClaims) + { + organizationClaims.Permissions.ManageGroups = true; + organizationClaims.Permissions = organizationClaims.Permissions.Invert(); + var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false)); + Assert.False(result); + } + + private class PermissionRequirement() : BasePermissionRequirement(_ => false); + private class TestCustomPermissionRequirement() : BasePermissionRequirement(p => p.ManageGroups); +} diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs b/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs new file mode 100644 index 0000000000..1acfbd5be3 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs @@ -0,0 +1,88 @@ +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Test.AdminConsole.Helpers; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization.Requirements; + +public class PermissionRequirementsTests +{ + /// + /// Correlates each IOrganizationRequirement with its custom permission. If you add a new requirement, + /// add a new entry here to have it automatically included in the tests below. + /// + public static IEnumerable RequirementData => new List + { + new object[] { new AccessEventLogsRequirement(), nameof(Permissions.AccessEventLogs) }, + new object[] { new AccessImportExportRequirement(), nameof(Permissions.AccessImportExport) }, + new object[] { new AccessReportsRequirement(), nameof(Permissions.AccessReports) }, + new object[] { new ManageAccountRecoveryRequirement(), nameof(Permissions.ManageResetPassword) }, + new object[] { new ManageGroupsRequirement(), nameof(Permissions.ManageGroups) }, + new object[] { new ManagePoliciesRequirement(), nameof(Permissions.ManagePolicies) }, + new object[] { new ManageScimRequirement(), nameof(Permissions.ManageScim) }, + new object[] { new ManageSsoRequirement(), nameof(Permissions.ManageSso) }, + new object[] { new ManageUsersRequirement(), nameof(Permissions.ManageUsers) }, + }; + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task Authorizes_Provider(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(true)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)] + public async Task Authorizes_Owner(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)] + public async Task Authorizes_Admin(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task Authorizes_Custom_With_Correct_Permission(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization) + { + organization.Permissions.SetPermission(permissionName, true); + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task DoesNotAuthorize_Custom_With_Other_Permissions(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization) + { + organization.Permissions.SetPermission(permissionName, true); + organization.Permissions = organization.Permissions.Invert(); + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.False(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task DoesNotAuthorize_User(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs index f346c47624..17045e29f0 100644 --- a/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs +++ b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs @@ -6,6 +6,23 @@ namespace Bit.Core.Test.AdminConsole.Helpers; public static class PermissionsHelpers { + /// + /// Sets the specified permission. + /// + /// The permission name specified as a string - using `nameof` is highly recommended. + /// The value to set the permission to. + /// No value; this mutates the permissions object. + public static void SetPermission(this Permissions permissions, string permissionName, bool value) + { + var prop = typeof(Permissions).GetProperty(permissionName); + if (prop == null) + { + throw new NullReferenceException("Invalid property name."); + } + + prop.SetValue(permissions, true); + } + /// /// Return a new Permission object with inverted permissions. /// This is useful to test negative cases, e.g. "all other permissions should fail". From 86ce3a86e9ec3d79c9b57a5424a18e9b2a96356b Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 31 Jul 2025 07:54:51 -0500 Subject: [PATCH 113/326] [PM-20452] - Offloading Stripe Update (#6034) * Adding job to update stripe subscriptions and increment seat count when inviting a user. * Updating name * Added ef migrations * Fixing script * Fixing procedures. Added repo tests. * Fixed set stored procedure. Fixed parameter name. * Added tests for database calls and updated stored procedures * Fixed build for sql file. * fixing sproc * File is nullsafe * Adding view to select from instead of table. * Updating UpdateSubscriptionStatus to use a CTE and do all the updates in 1 statement. * Setting revision date when incrementing seat count * Added feature flag check for the background job. * Fixing nullable property. * Removing new table and just adding the column to org. Updating to query and command. Updated tests. * Adding migration script rename * Add SyncSeats to Org.sql def * Adding contraint name * Removing old table files. * Added tests * Upped the frequency to be at the top of every 3rd hour. * Updating error message. * Removing extension method * Changed to GuidIdArray * Added xml doc and switched class to record --- .../Jobs/OrganizationSubscriptionUpdateJob.cs | 35 + src/Api/Jobs/JobsHostedService.cs | 10 +- .../AdminConsole/Entities/Organization.cs | 5 + .../OrganizationSubscriptionUpdate.cs | 11 + .../InviteOrganizationUsersCommand.cs | 18 +- .../InviteOrganizationUserValidator.cs | 5 +- ...tOrganizationSubscriptionsToUpdateQuery.cs | 24 + ...tOrganizationSubscriptionsToUpdateQuery.cs | 16 + .../IUpdateOrganizationSubscriptionCommand.cs | 16 + .../UpdateOrganizationSubscriptionCommand.cs | 43 + .../Repositories/IOrganizationRepository.cs | 26 + ...SubscriptionServiceCollectionExtensions.cs | 11 +- src/Core/Services/IPaymentService.cs | 8 + .../Repositories/OrganizationRepository.cs | 31 + .../OrganizationUserRepository.cs | 8 +- .../Repositories/OrganizationRepository.cs | 37 + .../Stored Procedures/Organization_Create.sql | 9 +- ...on_GetOrganizationsForSubscriptionSync.sql | 7 + .../Organization_IncrementSeatCount.sql | 15 + .../Stored Procedures/Organization_Update.sql | 6 +- .../Organization_UpdateSubscriptionStatus.sql | 14 + src/Sql/dbo/Tables/Organization.sql | 1 + .../OrganizationSubscriptionUpdateJobTests.cs | 60 + .../InviteOrganizationUserCommandTests.cs | 16 +- ...nizationSubscriptionsToUpdateQueryTests.cs | 54 + ...ateOrganizationSubscriptionCommandTests.cs | 146 + .../AdminConsole/OrganizationTestHelpers.cs | 4 +- .../OrganizationRepositoryTests.cs | 107 + .../2025-07-21_00_OrganizationSyncSeats.sql | 382 ++ ...16_Organization_Add_Sync_Seats.Designer.cs | 3266 ++++++++++++++++ ...50718154916_Organization_Add_Sync_Seats.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...06_Organization_Add_Sync_Seats.Designer.cs | 3272 +++++++++++++++++ ...50718154906_Organization_Add_Sync_Seats.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...11_Organization_Add_Sync_Seats.Designer.cs | 3255 ++++++++++++++++ ...50718154911_Organization_Add_Sync_Seats.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + 38 files changed, 10968 insertions(+), 43 deletions(-) create mode 100644 src/Api/AdminConsole/Jobs/OrganizationSubscriptionUpdateJob.cs create mode 100644 src/Core/AdminConsole/Models/Data/Organizations/OrganizationSubscriptionUpdate.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQuery.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IGetOrganizationSubscriptionsToUpdateQuery.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IUpdateOrganizationSubscriptionCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommand.cs create mode 100644 src/Sql/dbo/Stored Procedures/Organization_GetOrganizationsForSubscriptionSync.sql create mode 100644 src/Sql/dbo/Stored Procedures/Organization_IncrementSeatCount.sql create mode 100644 src/Sql/dbo/Stored Procedures/Organization_UpdateSubscriptionStatus.sql create mode 100644 test/Api.Test/AdminConsole/Jobs/OrganizationSubscriptionUpdateJobTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs create mode 100644 util/Migrator/DbScripts/2025-07-21_00_OrganizationSyncSeats.sql create mode 100644 util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.cs create mode 100644 util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.cs create mode 100644 util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.cs diff --git a/src/Api/AdminConsole/Jobs/OrganizationSubscriptionUpdateJob.cs b/src/Api/AdminConsole/Jobs/OrganizationSubscriptionUpdateJob.cs new file mode 100644 index 0000000000..3a6dbb22f4 --- /dev/null +++ b/src/Api/AdminConsole/Jobs/OrganizationSubscriptionUpdateJob.cs @@ -0,0 +1,35 @@ +using System.Collections.Immutable; +using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Jobs; +using Bit.Core.Services; +using Quartz; + +namespace Bit.Api.AdminConsole.Jobs; + +public class OrganizationSubscriptionUpdateJob(ILogger logger, + IGetOrganizationSubscriptionsToUpdateQuery query, + IUpdateOrganizationSubscriptionCommand command, + IFeatureService featureService) : BaseJob(logger) +{ + protected override async Task ExecuteJobAsync(IJobExecutionContext _) + { + if (!featureService.IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization)) + { + return; + } + + logger.LogInformation("OrganizationSubscriptionUpdateJob - START"); + + var organizationSubscriptionsToUpdate = + (await query.GetOrganizationSubscriptionsToUpdateAsync()) + .ToImmutableList(); + + logger.LogInformation("OrganizationSubscriptionUpdateJob - {numberOfOrganizations} organization(s) to update", + organizationSubscriptionsToUpdate.Count); + + await command.UpdateOrganizationSubscriptionAsync(organizationSubscriptionsToUpdate); + + logger.LogInformation("OrganizationSubscriptionUpdateJob - COMPLETED"); + } +} diff --git a/src/Api/Jobs/JobsHostedService.cs b/src/Api/Jobs/JobsHostedService.cs index 57b827a8be..0178f6d68b 100644 --- a/src/Api/Jobs/JobsHostedService.cs +++ b/src/Api/Jobs/JobsHostedService.cs @@ -1,4 +1,5 @@ -using Bit.Api.Auth.Jobs; +using Bit.Api.AdminConsole.Jobs; +using Bit.Api.Auth.Jobs; using Bit.Core.Jobs; using Bit.Core.Settings; using Quartz; @@ -65,6 +66,11 @@ public class JobsHostedService : BaseJobsHostedService .WithIntervalInHours(24) .RepeatForever()) .Build(); + var updateOrgSubscriptionsTrigger = TriggerBuilder.Create() + .WithIdentity("UpdateOrgSubscriptionsTrigger") + .StartNow() + .WithCronSchedule("0 0 */3 * * ?") // top of every 3rd hour + .Build(); var jobs = new List> @@ -76,6 +82,7 @@ public class JobsHostedService : BaseJobsHostedService new Tuple(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger), new Tuple(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger), new Tuple(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger), + new (typeof(OrganizationSubscriptionUpdateJob), updateOrgSubscriptionsTrigger), }; if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication) @@ -105,6 +112,7 @@ public class JobsHostedService : BaseJobsHostedService services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void AddCommercialSecretsManagerJobServices(IServiceCollection services) diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 8ab3af3c1e..3f02462501 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -123,6 +123,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable /// public bool UseAdminSponsoredFamilies { get; set; } + /// + /// If set to true, organization needs their seat count synced with their subscription + /// + public bool SyncSeats { get; set; } + public void SetNewId() { if (Id == default(Guid)) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationSubscriptionUpdate.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationSubscriptionUpdate.cs new file mode 100644 index 0000000000..ec66a6a94e --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationSubscriptionUpdate.cs @@ -0,0 +1,11 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.StaticStore; + +namespace Bit.Core.AdminConsole.Models.Data.Organizations; + +public record OrganizationSubscriptionUpdate +{ + public required Organization Organization { get; set; } + public int Seats => Organization.Seats ?? 0; + public Plan? Plan { get; set; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 47003be5c6..6899959b8d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -25,7 +25,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse public class InviteOrganizationUsersCommand(IEventService eventService, IOrganizationUserRepository organizationUserRepository, IInviteUsersValidator inviteUsersValidator, - IPaymentService paymentService, IOrganizationRepository organizationRepository, IApplicationCacheService applicationCacheService, IMailService mailService, @@ -190,12 +189,6 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { Seats: > 0, SeatsRequiredToAdd: > 0 }) { - - - await paymentService.AdjustSeatsAsync(organization, - validatedResult.Value.InviteOrganization.Plan, - validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats.Value); - organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats; await organizationRepository.ReplaceAsync(organization); @@ -297,13 +290,14 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { SeatsRequiredToAdd: > 0, UpdatedSeatTotal: > 0 }) { - await paymentService.AdjustSeatsAsync(organization, - validatedResult.Value.InviteOrganization.Plan, - validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal.Value); + await organizationRepository.IncrementSeatCountAsync( + organization.Id, + validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd, + validatedResult.Value.PerformedAt.UtcDateTime); - organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal; + organization.Seats = validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal; + organization.SyncSeats = true; - await organizationRepository.ReplaceAsync(organization); // could optimize this with only a property update await applicationCacheService.UpsertOrganizationAbilityAsync(organization); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index a3b1e43a04..f8bd988cab 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.AdminConsole.Utilities.Validation; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQuery.cs new file mode 100644 index 0000000000..faf435addd --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQuery.cs @@ -0,0 +1,24 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Billing.Pricing; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class GetOrganizationSubscriptionsToUpdateQuery(IOrganizationRepository organizationRepository, + IPricingClient pricingClient) : IGetOrganizationSubscriptionsToUpdateQuery +{ + public async Task> GetOrganizationSubscriptionsToUpdateAsync() + { + var organizationsToUpdateTask = organizationRepository.GetOrganizationsForSubscriptionSyncAsync(); + var plansTask = pricingClient.ListPlans(); + + await Task.WhenAll(organizationsToUpdateTask, plansTask); + + return organizationsToUpdateTask.Result.Select(o => new OrganizationSubscriptionUpdate + { + Organization = o, + Plan = plansTask.Result.FirstOrDefault(plan => plan.Type == o.PlanType) + }); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IGetOrganizationSubscriptionsToUpdateQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IGetOrganizationSubscriptionsToUpdateQuery.cs new file mode 100644 index 0000000000..e45a3ba957 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IGetOrganizationSubscriptionsToUpdateQuery.cs @@ -0,0 +1,16 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IGetOrganizationSubscriptionsToUpdateQuery +{ + /// + /// Retrieves a collection of organization subscriptions that need to be updated. This is based on if the + /// Organization.SyncSeats flag is true and Organization.Seats has a value. + /// + /// + /// A collection of instances, each representing an organization + /// subscription to be updated with their associated plan. + /// + Task> GetOrganizationSubscriptionsToUpdateAsync(); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IUpdateOrganizationSubscriptionCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IUpdateOrganizationSubscriptionCommand.cs new file mode 100644 index 0000000000..c8f5a15d39 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IUpdateOrganizationSubscriptionCommand.cs @@ -0,0 +1,16 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IUpdateOrganizationSubscriptionCommand +{ + /// + /// Attempts to update the subscription of all organizations that have had a subscription update. + /// + /// If successful, the Organization.SyncSeats flag will be set to false and Organization.RevisionDate will be set. + /// + /// In the event of a failure, it will log the failure and maybe be picked up in later runs. + /// + /// The collection of organization subscriptions to update. + Task UpdateOrganizationSubscriptionAsync(IEnumerable subscriptionsToUpdate); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommand.cs new file mode 100644 index 0000000000..450f425bdf --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommand.cs @@ -0,0 +1,43 @@ +using Bit.Core.AdminConsole.Models.Data.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class UpdateOrganizationSubscriptionCommand(IPaymentService paymentService, + IOrganizationRepository repository, + TimeProvider timeProvider, + ILogger logger) : IUpdateOrganizationSubscriptionCommand +{ + public async Task UpdateOrganizationSubscriptionAsync(IEnumerable subscriptionsToUpdate) + { + var successfulSyncs = new List(); + + foreach (var subscriptionUpdate in subscriptionsToUpdate) + { + try + { + await paymentService.AdjustSeatsAsync(subscriptionUpdate.Organization, + subscriptionUpdate.Plan, + subscriptionUpdate.Seats); + + successfulSyncs.Add(subscriptionUpdate.Organization.Id); + } + catch (Exception ex) + { + logger.LogError(ex, + "Failed to update organization {organizationId} subscription.", + subscriptionUpdate.Organization.Id); + } + } + + if (successfulSyncs.Count == 0) + { + return; + } + + await repository.UpdateSuccessfulOrganizationSyncStatusAsync(successfulSyncs, timeProvider.GetUtcNow().UtcDateTime); + } +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs index 7fff0d437f..da7a77000b 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationRepository.cs @@ -24,6 +24,7 @@ public interface IOrganizationRepository : IRepository /// Gets the organizations that have a verified domain matching the user's email domain. /// Task> GetByVerifiedUserEmailDomainAsync(Guid userId); + Task> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType); Task> GetManyByIdsAsync(IEnumerable ids); @@ -36,4 +37,29 @@ public interface IOrganizationRepository : IRepository /// The ID of the organization to get the occupied seat count for. /// The number of occupied seats for the organization. Task GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId); + + /// + /// Get all organizations that need to have their seat count updated to their Stripe subscription. + /// + /// Organizations to sync to Stripe + Task> GetOrganizationsForSubscriptionSyncAsync(); + + /// + /// Updates the organization SeatSync property to signify the organization's subscription has been updated in stripe + /// to match the password manager seats for the organization. + /// + /// + /// + /// + Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable successfulOrganizations, DateTime syncDate); + + /// + /// This increments the password manager seat count on the organization by the provided amount and sets SyncSeats to true. + /// It also sets the revision date using the request date. + /// + /// Organization to update + /// Amount to increase password manager seats by + /// When the action was performed + /// + Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate); } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs index 2e65fd0563..ef12e1d0f1 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/OrganizationSubscriptionServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ -using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -7,7 +9,10 @@ public static class OrganizationSubscriptionServiceCollectionExtensions { public static void AddOrganizationSubscriptionServices(this IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); + services + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(); } } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 9b56399add..e7e848bcba 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -25,6 +25,14 @@ public interface IPaymentService int? newlyPurchasedSecretsManagerSeats, int? newlyPurchasedAdditionalSecretsManagerServiceAccounts, int newlyPurchasedAdditionalStorage); + + /// + /// Used to update the organization's password manager subscription + /// + /// + /// + /// New seat total + /// Task AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats); Task AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs index 27a08df3ed..96ddc8c7da 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationRepository.cs @@ -220,4 +220,35 @@ public class OrganizationRepository : Repository, IOrganizat return result.SingleOrDefault() ?? new OrganizationSeatCounts(); } } + + public async Task> GetOrganizationsForSubscriptionSyncAsync() + { + await using var connection = new SqlConnection(ConnectionString); + + return await connection.QueryAsync( + "[dbo].[Organization_GetOrganizationsForSubscriptionSync]", + commandType: CommandType.StoredProcedure); + } + + public async Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable successfulOrganizations, DateTime syncDate) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync("[dbo].[Organization_UpdateSubscriptionStatus]", + new + { + SuccessfulOrganizations = successfulOrganizations.ToGuidIdArrayTVP(), + SyncDate = syncDate + }, + commandType: CommandType.StoredProcedure); + } + + public async Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync("[dbo].[Organization_IncrementSeatCount]", + new { OrganizationId = organizationId, SeatsToAdd = increaseAmount, RequestDate = requestDate }, + commandType: CommandType.StoredProcedure); + } } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 2b9298a75a..8666b5307f 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -660,12 +660,14 @@ public class OrganizationUserRepository : Repository, IO { await using var connection = new SqlConnection(_marsConnectionString); + var organizationUsersList = organizationUserCollection.ToList(); + await connection.ExecuteAsync( $"[{Schema}].[OrganizationUser_CreateManyWithCollectionsAndGroups]", new { - OrganizationUserData = JsonSerializer.Serialize(organizationUserCollection.Select(x => x.OrganizationUser)), - CollectionData = JsonSerializer.Serialize(organizationUserCollection + OrganizationUserData = JsonSerializer.Serialize(organizationUsersList.Select(x => x.OrganizationUser)), + CollectionData = JsonSerializer.Serialize(organizationUsersList .SelectMany(x => x.Collections, (user, collection) => new CollectionUser { CollectionId = collection.Id, @@ -674,7 +676,7 @@ public class OrganizationUserRepository : Repository, IO HidePasswords = collection.HidePasswords, Manage = collection.Manage })), - GroupData = JsonSerializer.Serialize(organizationUserCollection + GroupData = JsonSerializer.Serialize(organizationUsersList .SelectMany(x => x.Groups, (user, group) => new GroupUser { GroupId = group, diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 53216b9d78..200c4aa308 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -403,4 +403,41 @@ public class OrganizationRepository : Repository> GetOrganizationsForSubscriptionSyncAsync() + { + using var scope = ServiceScopeFactory.CreateScope(); + await using var dbContext = GetDatabaseContext(scope); + + var organizations = await dbContext.Organizations + .Where(o => o.SyncSeats == true && o.Seats != null) + .ToArrayAsync(); + + return organizations; + } + + public async Task UpdateSuccessfulOrganizationSyncStatusAsync(IEnumerable successfulOrganizations, DateTime syncDate) + { + using var scope = ServiceScopeFactory.CreateScope(); + await using var dbContext = GetDatabaseContext(scope); + + await dbContext.Organizations + .Where(o => successfulOrganizations.Contains(o.Id)) + .ExecuteUpdateAsync(o => o + .SetProperty(x => x.SyncSeats, false) + .SetProperty(x => x.RevisionDate, syncDate.Date)); + } + + public async Task IncrementSeatCountAsync(Guid organizationId, int increaseAmount, DateTime requestDate) + { + using var scope = ServiceScopeFactory.CreateScope(); + await using var dbContext = GetDatabaseContext(scope); + + await dbContext.Organizations + .Where(o => o.Id == organizationId) + .ExecuteUpdateAsync(s => s + .SetProperty(o => o.Seats, o => o.Seats + increaseAmount) + .SetProperty(o => o.SyncSeats, true) + .SetProperty(o => o.RevisionDate, requestDate)); + } } diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index dc793351f7..295ebb51a8 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -57,7 +57,8 @@ CREATE PROCEDURE [dbo].[Organization_Create] @UseRiskInsights BIT = 0, @LimitItemDeletion BIT = 0, @UseOrganizationDomains BIT = 0, - @UseAdminSponsoredFamilies BIT = 0 + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0 AS BEGIN SET NOCOUNT ON @@ -122,7 +123,8 @@ BEGIN [UseRiskInsights], [LimitItemDeletion], [UseOrganizationDomains], - [UseAdminSponsoredFamilies] + [UseAdminSponsoredFamilies], + [SyncSeats] ) VALUES ( @@ -184,6 +186,7 @@ BEGIN @UseRiskInsights, @LimitItemDeletion, @UseOrganizationDomains, - @UseAdminSponsoredFamilies + @UseAdminSponsoredFamilies, + @SyncSeats ) END diff --git a/src/Sql/dbo/Stored Procedures/Organization_GetOrganizationsForSubscriptionSync.sql b/src/Sql/dbo/Stored Procedures/Organization_GetOrganizationsForSubscriptionSync.sql new file mode 100644 index 0000000000..1c0e8c01ef --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_GetOrganizationsForSubscriptionSync.sql @@ -0,0 +1,7 @@ +CREATE PROCEDURE [dbo].[Organization_GetOrganizationsForSubscriptionSync] +AS +BEGIN + SELECT * + FROM [dbo].[OrganizationView] + WHERE [Seats] IS NOT NULL AND [SyncSeats] = 1 +END diff --git a/src/Sql/dbo/Stored Procedures/Organization_IncrementSeatCount.sql b/src/Sql/dbo/Stored Procedures/Organization_IncrementSeatCount.sql new file mode 100644 index 0000000000..7e3eba1e13 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_IncrementSeatCount.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[Organization_IncrementSeatCount] + @OrganizationId UNIQUEIDENTIFIER, + @SeatsToAdd INT, + @RequestDate DATETIME2 +AS +BEGIN + SET NOCOUNT ON + + UPDATE [dbo].[Organization] + SET + [Seats] = [Seats] + @SeatsToAdd, + [SyncSeats] = 1, + [RevisionDate] = @RequestDate + WHERE [Id] = @OrganizationId +END diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 0043993686..d60852bab6 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -57,7 +57,8 @@ CREATE PROCEDURE [dbo].[Organization_Update] @UseRiskInsights BIT = 0, @LimitItemDeletion BIT = 0, @UseOrganizationDomains BIT = 0, - @UseAdminSponsoredFamilies BIT = 0 + @UseAdminSponsoredFamilies BIT = 0, + @SyncSeats BIT = 0 AS BEGIN SET NOCOUNT ON @@ -122,7 +123,8 @@ BEGIN [UseRiskInsights] = @UseRiskInsights, [LimitItemDeletion] = @LimitItemDeletion, [UseOrganizationDomains] = @UseOrganizationDomains, - [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies + [UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies, + [SyncSeats] = @SyncSeats WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Stored Procedures/Organization_UpdateSubscriptionStatus.sql b/src/Sql/dbo/Stored Procedures/Organization_UpdateSubscriptionStatus.sql new file mode 100644 index 0000000000..224e76f5dd --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Organization_UpdateSubscriptionStatus.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[Organization_UpdateSubscriptionStatus] + @SuccessfulOrganizations AS [dbo].[GuidIdArray] READONLY, + @SyncDate DATETIME2 +AS +BEGIN + SET NOCOUNT ON + + UPDATE o + SET + [SyncSeats] = 0, + [RevisionDate] = @SyncDate + FROM [dbo].[Organization] o + INNER JOIN @SuccessfulOrganizations success on success.Id = o.Id +END diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index 2accd2134b..897abef1cf 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -58,6 +58,7 @@ CREATE TABLE [dbo].[Organization] ( [UseRiskInsights] BIT NOT NULL CONSTRAINT [DF_Organization_UseRiskInsights] DEFAULT (0), [UseOrganizationDomains] BIT NOT NULL CONSTRAINT [DF_Organization_UseOrganizationDomains] DEFAULT (0), [UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0), + [SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0), CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/test/Api.Test/AdminConsole/Jobs/OrganizationSubscriptionUpdateJobTests.cs b/test/Api.Test/AdminConsole/Jobs/OrganizationSubscriptionUpdateJobTests.cs new file mode 100644 index 0000000000..e500fcae1d --- /dev/null +++ b/test/Api.Test/AdminConsole/Jobs/OrganizationSubscriptionUpdateJobTests.cs @@ -0,0 +1,60 @@ +using Bit.Api.AdminConsole.Jobs; +using Bit.Core; +using Bit.Core.AdminConsole.Models.Data.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Quartz; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Jobs; + +[SutProviderCustomize] +public class OrganizationSubscriptionUpdateJobTests +{ + [Theory] + [BitAutoData] + public async Task ExecuteJobAsync_WhenScimInviteUserIsDisabled_ThenQueryAndCommandAreNotExecuted( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization) + .Returns(false); + + var contextMock = Substitute.For(); + + await sutProvider.Sut.Execute(contextMock); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationSubscriptionsToUpdateAsync(); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationSubscriptionAsync(Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task ExecuteJobAsync_WhenScimInviteUserIsEnabled_ThenQueryAndCommandAreExecuted( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.ScimInviteUserOptimization) + .Returns(true); + + var contextMock = Substitute.For(); + + await sutProvider.Sut.Execute(contextMock); + + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationSubscriptionsToUpdateAsync(); + + await sutProvider.GetDependency() + .Received(1) + .UpdateOrganizationSubscriptionAsync(Arg.Any>()); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index aa803bd0c9..10dcff9e2a 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -19,7 +19,6 @@ using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -81,10 +80,6 @@ public class InviteOrganizationUserCommandTests Assert.IsType>(result); Assert.Equal(NoUsersToInviteError.Code, (result as Failure)!.Error.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .AdjustSeatsAsync(Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .SendInvitesAsync(Arg.Any()); @@ -458,10 +453,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - await sutProvider.GetDependency() - .AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.UpdatedSeatTotal!.Value); - - await orgRepository.Received(1).ReplaceAsync(Arg.Is(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal)); + await orgRepository.Received(1).IncrementSeatCountAsync(organization.Id, passwordManagerUpdate.SeatsRequiredToAdd, request.PerformedAt.UtcDateTime); await sutProvider.GetDependency() .Received(1) @@ -632,11 +624,7 @@ public class InviteOrganizationUserCommandTests .UpdateSubscriptionAsync(Arg.Any()); // PM revert - await sutProvider.GetDependency() - .Received(2) - .AdjustSeatsAsync(Arg.Any(), Arg.Any(), Arg.Any()); - - await orgRepository.Received(2).ReplaceAsync(Arg.Any()); + await orgRepository.Received(1).ReplaceAsync(Arg.Any()); await sutProvider.GetDependency().Received(2) .UpsertOrganizationAbilityAsync(Arg.Any()); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs new file mode 100644 index 0000000000..af6b5a17f7 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/GetOrganizationSubscriptionsToUpdateQueryTests.cs @@ -0,0 +1,54 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Billing.Pricing; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class GetOrganizationSubscriptionsToUpdateQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenNoOrganizationsNeedToBeSynced_ThenAnEmptyListIsReturned( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetOrganizationsForSubscriptionSyncAsync() + .Returns([]); + + var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync(); + + Assert.Empty(result); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationSubscriptionsToUpdateAsync_WhenOrganizationsNeedToBeSynced_ThenUpdateIsReturnedWithCorrectPlanAndOrg( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.EnterpriseAnnually2023; + + sutProvider.GetDependency() + .GetOrganizationsForSubscriptionSyncAsync() + .Returns([organization]); + + sutProvider.GetDependency() + .ListPlans() + .Returns([new Enterprise2023Plan(true)]); + + var result = await sutProvider.Sut.GetOrganizationSubscriptionsToUpdateAsync(); + + var matchingUpdate = result.FirstOrDefault(x => x.Organization.Id == organization.Id); + Assert.NotNull(matchingUpdate); + Assert.Equal(organization.PlanType, matchingUpdate.Plan!.Type); + Assert.Equal(organization, matchingUpdate.Organization); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs new file mode 100644 index 0000000000..37a5627919 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/UpdateOrganizationSubscriptionCommandTests.cs @@ -0,0 +1,146 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Organizations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class UpdateOrganizationSubscriptionCommandTests +{ + [Theory] + [BitAutoData] + public async Task UpdateOrganizationSubscriptionAsync_WhenNoSubscriptionsNeedToBeUpdated_ThenNoSyncsOccur( + SutProvider sutProvider) + { + // Arrange + OrganizationSubscriptionUpdate[] subscriptionsToUpdate = []; + + // Act + await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate); + + await sutProvider.GetDependency() + .DidNotReceive() + .AdjustSeatsAsync(Arg.Any(), Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any>(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationSubscriptionAsync_WhenOrgUpdatePassedIn_ThenSyncedThroughPaymentService( + Organization organization, + SutProvider sutProvider) + { + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually2023; + organization.Seats = 2; + + OrganizationSubscriptionUpdate[] subscriptionsToUpdate = + [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }]; + + // Act + await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate); + + await sutProvider.GetDependency() + .Received(1) + .AdjustSeatsAsync( + Arg.Is(x => x.Id == organization.Id), + Arg.Is(x => x.Type == organization.PlanType), + organization.Seats!.Value); + + await sutProvider.GetDependency() + .Received(1) + .UpdateSuccessfulOrganizationSyncStatusAsync( + Arg.Is>(x => x.Contains(organization.Id)), + Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationSubscriptionAsync_WhenOrgUpdateFails_ThenSyncDoesNotOccur( + Organization organization, + Exception exception, + SutProvider sutProvider) + { + // Arrange + organization.PlanType = PlanType.EnterpriseAnnually2023; + organization.Seats = 2; + + OrganizationSubscriptionUpdate[] subscriptionsToUpdate = + [new() { Organization = organization, Plan = new Enterprise2023Plan(true) }]; + + sutProvider.GetDependency() + .AdjustSeatsAsync( + Arg.Is(x => x.Id == organization.Id), + Arg.Is(x => x.Type == organization.PlanType), + organization.Seats!.Value).ThrowsAsync(exception); + + // Act + await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateSuccessfulOrganizationSyncStatusAsync(Arg.Any>(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationSubscriptionAsync_WhenOneOrgUpdateFailsAndAnotherSucceeds_ThenSyncOccursForTheSuccessfulOrg( + Organization successfulOrganization, + Organization failedOrganization, + Exception exception, + SutProvider sutProvider) + { + // Arrange + successfulOrganization.PlanType = PlanType.EnterpriseAnnually2023; + successfulOrganization.Seats = 2; + failedOrganization.PlanType = PlanType.EnterpriseAnnually2023; + failedOrganization.Seats = 2; + + OrganizationSubscriptionUpdate[] subscriptionsToUpdate = + [ + new() { Organization = successfulOrganization, Plan = new Enterprise2023Plan(true) }, + new() { Organization = failedOrganization, Plan = new Enterprise2023Plan(true) } + ]; + + sutProvider.GetDependency() + .AdjustSeatsAsync( + Arg.Is(x => x.Id == failedOrganization.Id), + Arg.Is(x => x.Type == failedOrganization.PlanType), + failedOrganization.Seats!.Value).ThrowsAsync(exception); + + // Act + await sutProvider.Sut.UpdateOrganizationSubscriptionAsync(subscriptionsToUpdate); + + await sutProvider.GetDependency() + .Received(1) + .AdjustSeatsAsync( + Arg.Is(x => x.Id == successfulOrganization.Id), + Arg.Is(x => x.Type == successfulOrganization.PlanType), + successfulOrganization.Seats!.Value); + + await sutProvider.GetDependency() + .Received(1) + .UpdateSuccessfulOrganizationSyncStatusAsync( + Arg.Is>(x => x.Contains(successfulOrganization.Id)), + Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateSuccessfulOrganizationSyncStatusAsync( + Arg.Is>(x => x.Contains(failedOrganization.Id)), + Arg.Any()); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index 10361877d8..2aee528260 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -31,13 +31,15 @@ public static class OrganizationTestHelpers /// Creates an Enterprise organization. /// public static Task CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository, + int? seatCount = null, string identifier = "test") => organizationRepository.CreateAsync(new Organization { Name = $"{identifier}-{Guid.NewGuid()}", BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl - PlanType = PlanType.EnterpriseAnnually + PlanType = PlanType.EnterpriseAnnually, + Seats = seatCount }); /// diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index a0df63c94e..ae30fb4bed 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -423,4 +423,111 @@ public class OrganizationRepositoryTests Assert.Equal(0, result.Sponsored); Assert.Equal(0, result.Total); } + + [DatabaseTheory, DatabaseData] + public async Task IncrementSeatCountAsync_IncrementsSeatCount(IOrganizationRepository organizationRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + organization.Seats = 5; + await organizationRepository.ReplaceAsync(organization); + + await organizationRepository.IncrementSeatCountAsync(organization.Id, 3, DateTime.UtcNow); + + var result = await organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(result); + Assert.Equal(8, result.Seats); + } + + [DatabaseData, DatabaseTheory] + public async Task IncrementSeatCountAsync_GivenOrganizationHasNotChangedSeatCountBefore_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved( + IOrganizationRepository sutRepository) + { + // Arrange + var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); + var requestDate = DateTime.UtcNow; + + // Act + await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); + + // Assert + var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); + + var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); + Assert.NotNull(updateResult); + Assert.Equal(organization.Id, updateResult.Id); + Assert.True(updateResult.SyncSeats); + Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); + + // Annul + await sutRepository.DeleteAsync(organization); + } + + [DatabaseData, DatabaseTheory] + public async Task IncrementSeatCountAsync_GivenOrganizationHasChangedSeatCountBeforeAndRecordExists_WhenUpdatingOrgSeats_ThenSubscriptionUpdateIsSaved( + IOrganizationRepository sutRepository) + { + // Arrange + var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); + await sutRepository.IncrementSeatCountAsync(organization.Id, 1, DateTime.UtcNow); + + var requestDate = DateTime.UtcNow; + + // Act + await sutRepository.IncrementSeatCountAsync(organization.Id, 1, DateTime.UtcNow); + + // Assert + var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); + var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); + Assert.NotNull(updateResult); + Assert.Equal(organization.Id, updateResult.Id); + Assert.True(updateResult.SyncSeats); + Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); + + // Annul + await sutRepository.DeleteAsync(organization); + } + + [DatabaseData, DatabaseTheory] + public async Task GetOrganizationsForSubscriptionSyncAsync_GivenOrganizationHasChangedSeatCount_WhenGettingOrgsToUpdate_ThenReturnsOrgSubscriptionUpdate( + IOrganizationRepository sutRepository) + { + // Arrange + var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); + var requestDate = DateTime.UtcNow; + await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); + + // Act + var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); + + // Assert + var updateResult = result.FirstOrDefault(x => x.Id == organization.Id); + Assert.NotNull(updateResult); + Assert.Equal(organization.Id, updateResult.Id); + Assert.True(updateResult.SyncSeats); + Assert.Equal(requestDate.ToString("yyyy-MM-dd HH:mm:ss"), updateResult.RevisionDate.ToString("yyyy-MM-dd HH:mm:ss")); + + // Annul + await sutRepository.DeleteAsync(organization); + } + + [DatabaseData, DatabaseTheory] + public async Task UpdateSuccessfulOrganizationSyncStatusAsync_GivenOrganizationHasChangedSeatCount_WhenUpdatingStatus_ThenSuccessfullyUpdatesOrgSoItDoesntSync( + IOrganizationRepository sutRepository) + { + // Arrange + var organization = await sutRepository.CreateTestOrganizationAsync(seatCount: 2); + var requestDate = DateTime.UtcNow; + var syncDate = DateTime.UtcNow.AddMinutes(1); + await sutRepository.IncrementSeatCountAsync(organization.Id, 1, requestDate); + + // Act + await sutRepository.UpdateSuccessfulOrganizationSyncStatusAsync([organization.Id], syncDate); + + // Assert + var result = (await sutRepository.GetOrganizationsForSubscriptionSyncAsync()).ToArray(); + Assert.Null(result.FirstOrDefault(x => x.Id == organization.Id)); + + // Annul + await sutRepository.DeleteAsync(organization); + } } diff --git a/util/Migrator/DbScripts/2025-07-21_00_OrganizationSyncSeats.sql b/util/Migrator/DbScripts/2025-07-21_00_OrganizationSyncSeats.sql new file mode 100644 index 0000000000..0193f8692d --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-21_00_OrganizationSyncSeats.sql @@ -0,0 +1,382 @@ +-- Add the new column if it doesn't exist +IF NOT EXISTS (SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = 'Organization' + AND COLUMN_NAME = 'SyncSeats') + BEGIN + ALTER TABLE [dbo].[Organization] + ADD [SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT 0; + END +GO + +-- Refresh view +EXEC sp_refreshsqlmodule N'[dbo].[OrganizationView]'; +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_GetOrganizationsForSubscriptionSync] +AS +BEGIN + SELECT * + FROM [dbo].[OrganizationView] + WHERE [Seats] IS NOT NULL AND [SyncSeats] = 1 +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_UpdateSubscriptionStatus] + @SuccessfulOrganizations AS [dbo].[GuidIdArray] READONLY, + @SyncDate DATETIME2 +AS +BEGIN + SET NOCOUNT ON + + UPDATE o + SET + [SyncSeats] = 0, + [RevisionDate] = @SyncDate + FROM [dbo].[Organization] o + INNER JOIN @SuccessfulOrganizations success on success.Id = o.Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Organization_IncrementSeatCount] + @OrganizationId UNIQUEIDENTIFIER, + @SeatsToAdd INT, + @RequestDate DATETIME2 +AS +BEGIN + SET NOCOUNT ON + + UPDATE [dbo].[Organization] + SET + [Seats] = [Seats] + @SeatsToAdd, + [SyncSeats] = 1, + [RevisionDate] = @RequestDate + WHERE [Id] = @OrganizationId +END +GO + +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 +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] + ) + 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 + ) +END +GO + +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 +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 + WHERE + [Id] = @Id +END +GO diff --git a/util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.Designer.cs b/util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.Designer.cs new file mode 100644 index 0000000000..bac28a1ec9 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.Designer.cs @@ -0,0 +1,3266 @@ +// +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("20250718154916_Organization_Add_Sync_Seats")] + partial class Organization_Add_Sync_Seats + { + /// + 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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250718154916_Organization_Add_Sync_Seats.cs b/util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.cs new file mode 100644 index 0000000000..2a1872d42d --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250718154916_Organization_Add_Sync_Seats.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class Organization_Add_Sync_Seats : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SyncSeats", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SyncSeats", + table: "Organization"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 89246bcff0..1b0bf84bfc 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -205,6 +205,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Storage") .HasColumnType("bigint"); + b.Property("SyncSeats") + .HasColumnType("tinyint(1)"); + b.Property("TwoFactorProviders") .HasColumnType("longtext"); diff --git a/util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.Designer.cs b/util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.Designer.cs new file mode 100644 index 0000000000..60e0fc4404 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.Designer.cs @@ -0,0 +1,3272 @@ +// +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("20250718154906_Organization_Add_Sync_Seats")] + partial class Organization_Add_Sync_Seats + { + /// + 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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250718154906_Organization_Add_Sync_Seats.cs b/util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.cs new file mode 100644 index 0000000000..1ce5428090 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250718154906_Organization_Add_Sync_Seats.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class Organization_Add_Sync_Seats : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SyncSeats", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SyncSeats", + table: "Organization"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 349028da7e..2238770810 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -207,6 +207,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Storage") .HasColumnType("bigint"); + b.Property("SyncSeats") + .HasColumnType("boolean"); + b.Property("TwoFactorProviders") .HasColumnType("text"); diff --git a/util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.Designer.cs b/util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.Designer.cs new file mode 100644 index 0000000000..c31ee5f063 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.Designer.cs @@ -0,0 +1,3255 @@ +// +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("20250718154911_Organization_Add_Sync_Seats")] + partial class Organization_Add_Sync_Seats + { + /// + 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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250718154911_Organization_Add_Sync_Seats.cs b/util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.cs new file mode 100644 index 0000000000..92731046fd --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250718154911_Organization_Add_Sync_Seats.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class Organization_Add_Sync_Seats : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SyncSeats", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SyncSeats", + table: "Organization"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 62fcd433aa..41a179d1b5 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -200,6 +200,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Storage") .HasColumnType("INTEGER"); + b.Property("SyncSeats") + .HasColumnType("INTEGER"); + b.Property("TwoFactorProviders") .HasColumnType("TEXT"); From ff5659cc0f25ec43060ecf3958d7e59b79497050 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 31 Jul 2025 11:24:16 -0400 Subject: [PATCH 114/326] Add bulk default collection creation method (#6075) --- .../Repositories/ICollectionRepository.cs | 2 + .../Helpers/BulkResourceCreationService.cs | 129 +++++++++++++++ .../Repositories/CollectionRepository.cs | 98 +++++++++++ .../Repositories/CollectionRepository.cs | 94 ++++++++++- .../Queries/UserCollectionDetailsQuery.cs | 21 +-- .../CreateDefaultCollectionsTests.cs | 154 ++++++++++++++++++ 6 files changed, 486 insertions(+), 12 deletions(-) create mode 100644 src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 9e2f253c9f..70bda3eb13 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -62,4 +62,6 @@ public interface ICollectionRepository : IRepository Task DeleteManyAsync(IEnumerable collectionIds); Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable collectionIds, IEnumerable users, IEnumerable groups); + + Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs new file mode 100644 index 0000000000..139960ceba --- /dev/null +++ b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs @@ -0,0 +1,129 @@ +using System.Data; +using Bit.Core.Entities; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers; + +public static class BulkResourceCreationService +{ + private const string _defaultErrorMessage = "Must have at least one record for bulk creation."; + public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable collectionUsers, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "[dbo].[CollectionUser]"; + var dataTable = BuildCollectionsUsersTable(bulkCopy, collectionUsers, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + + private static DataTable BuildCollectionsUsersTable(SqlBulkCopy bulkCopy, IEnumerable collectionUsers, string errorMessage) + { + var collectionUser = collectionUsers.FirstOrDefault(); + + if (collectionUser == null) + { + throw new ApplicationException(errorMessage); + } + + var table = new DataTable("CollectionUserDataTable"); + + var collectionIdColumn = new DataColumn(nameof(collectionUser.CollectionId), collectionUser.CollectionId.GetType()); + table.Columns.Add(collectionIdColumn); + var orgUserIdColumn = new DataColumn(nameof(collectionUser.OrganizationUserId), collectionUser.OrganizationUserId.GetType()); + table.Columns.Add(orgUserIdColumn); + var readOnlyColumn = new DataColumn(nameof(collectionUser.ReadOnly), collectionUser.ReadOnly.GetType()); + table.Columns.Add(readOnlyColumn); + var hidePasswordsColumn = new DataColumn(nameof(collectionUser.HidePasswords), collectionUser.HidePasswords.GetType()); + table.Columns.Add(hidePasswordsColumn); + var manageColumn = new DataColumn(nameof(collectionUser.Manage), collectionUser.Manage.GetType()); + table.Columns.Add(manageColumn); + + foreach (DataColumn col in table.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + var keys = new DataColumn[2]; + keys[0] = collectionIdColumn; + keys[1] = orgUserIdColumn; + table.PrimaryKey = keys; + + foreach (var collectionUserRecord in collectionUsers) + { + var row = table.NewRow(); + + row[collectionIdColumn] = collectionUserRecord.CollectionId; + row[orgUserIdColumn] = collectionUserRecord.OrganizationUserId; + row[readOnlyColumn] = collectionUserRecord.ReadOnly; + row[hidePasswordsColumn] = collectionUserRecord.HidePasswords; + row[manageColumn] = collectionUserRecord.Manage; + + table.Rows.Add(row); + } + + return table; + } + + public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable collections, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "[dbo].[Collection]"; + var dataTable = BuildCollectionsTable(bulkCopy, collections, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + + private static DataTable BuildCollectionsTable(SqlBulkCopy bulkCopy, IEnumerable collections, string errorMessage) + { + var collection = collections.FirstOrDefault(); + + if (collection == null) + { + throw new ApplicationException(errorMessage); + } + + var collectionsTable = new DataTable("CollectionDataTable"); + + var idColumn = new DataColumn(nameof(collection.Id), collection.Id.GetType()); + collectionsTable.Columns.Add(idColumn); + var organizationIdColumn = new DataColumn(nameof(collection.OrganizationId), collection.OrganizationId.GetType()); + collectionsTable.Columns.Add(organizationIdColumn); + var nameColumn = new DataColumn(nameof(collection.Name), collection.Name.GetType()); + collectionsTable.Columns.Add(nameColumn); + var creationDateColumn = new DataColumn(nameof(collection.CreationDate), collection.CreationDate.GetType()); + collectionsTable.Columns.Add(creationDateColumn); + var revisionDateColumn = new DataColumn(nameof(collection.RevisionDate), collection.RevisionDate.GetType()); + collectionsTable.Columns.Add(revisionDateColumn); + var externalIdColumn = new DataColumn(nameof(collection.ExternalId), typeof(string)); + collectionsTable.Columns.Add(externalIdColumn); + var typeColumn = new DataColumn(nameof(collection.Type), collection.Type.GetType()); + collectionsTable.Columns.Add(typeColumn); + var defaultUserCollectionEmailColumn = new DataColumn(nameof(collection.DefaultUserCollectionEmail), typeof(string)); + collectionsTable.Columns.Add(defaultUserCollectionEmailColumn); + + foreach (DataColumn col in collectionsTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + var keys = new DataColumn[1]; + keys[0] = idColumn; + collectionsTable.PrimaryKey = keys; + + foreach (var collectionRecord in collections) + { + var row = collectionsTable.NewRow(); + + row[idColumn] = collectionRecord.Id; + row[organizationIdColumn] = collectionRecord.OrganizationId; + row[nameColumn] = collectionRecord.Name; + row[creationDateColumn] = collectionRecord.CreationDate; + row[revisionDateColumn] = collectionRecord.RevisionDate; + row[externalIdColumn] = collectionRecord.ExternalId; + row[typeColumn] = collectionRecord.Type; + row[defaultUserCollectionEmailColumn] = collectionRecord.DefaultUserCollectionEmail; + + collectionsTable.Rows.Add(row); + } + + return collectionsTable; + } +} diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index 6b71b57e3d..77fbdff3ae 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -2,9 +2,11 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.AdminConsole.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -222,6 +224,8 @@ public class CollectionRepository : Repository, ICollectionRep public async Task CreateAsync(Collection obj, IEnumerable? groups, IEnumerable? users) { obj.SetNewId(); + + var objWithGroupsAndUsers = JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; objWithGroupsAndUsers.Groups = groups != null ? groups.ToArrayTVP() : Enumerable.Empty().ToArrayTVP(); @@ -322,6 +326,100 @@ public class CollectionRepository : Repository, ICollectionRep } } + public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName) + { + if (!affectedOrgUserIds.Any()) + { + return; + } + + await using var connection = new SqlConnection(ConnectionString); + connection.Open(); + await using var transaction = connection.BeginTransaction(); + try + { + var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(connection, transaction, organizationId); + + var missingDefaultCollectionUserIds = affectedOrgUserIds.Except(orgUserIdWithDefaultCollection); + + var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); + + if (!collectionUsers.Any() || !collections.Any()) + { + return; + } + + await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections); + await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + + private async Task> GetOrgUserIdsWithDefaultCollectionAsync(SqlConnection connection, SqlTransaction transaction, Guid organizationId) + { + const string sql = @" + SELECT + ou.Id AS OrganizationUserId + FROM + OrganizationUser ou + INNER JOIN + CollectionUser cu ON cu.OrganizationUserId = ou.Id + INNER JOIN + Collection c ON c.Id = cu.CollectionId + WHERE + ou.OrganizationId = @OrganizationId + AND c.Type = @CollectionType; + "; + + var organizationUserIds = await connection.QueryAsync( + sql, + new { OrganizationId = organizationId, CollectionType = CollectionType.DefaultUserCollection }, + transaction: transaction + ); + + return organizationUserIds.ToHashSet(); + } + + private (List collectionUser, List collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable missingDefaultCollectionUserIds, string defaultCollectionName) + { + var collectionUsers = new List(); + var collections = new List(); + + foreach (var orgUserId in missingDefaultCollectionUserIds) + { + var collectionId = Guid.NewGuid(); + + collections.Add(new Collection + { + Id = collectionId, + OrganizationId = organizationId, + Name = defaultCollectionName, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = null + + }); + + collectionUsers.Add(new CollectionUser + { + CollectionId = collectionId, + OrganizationUserId = orgUserId, + ReadOnly = false, + HidePasswords = false, + Manage = true, + }); + } + + return (collectionUsers, collections); + } + public class CollectionWithGroupsAndUsers : Collection { [DisallowNull] diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 3169f86420..9f047e4653 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -4,6 +4,7 @@ using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories.Queries; +using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -256,7 +257,8 @@ public class CollectionRepository : Repository new CollectionDetails { @@ -269,6 +271,7 @@ public class CollectionRepository : Repository Convert.ToInt32(c.ReadOnly))), HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), + Type = collectionGroup.Key.Type, }) .ToList(); } @@ -281,7 +284,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.ReadOnly))), HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), + Type = collectionGroup.Key.Type, }).ToListAsync(); } } @@ -711,6 +716,7 @@ public class CollectionRepository : Repository groups) { var existingCollectionGroups = await dbContext.CollectionGroups @@ -782,4 +788,88 @@ public class CollectionRepository : Repository affectedOrgUserIds, string defaultCollectionName) + { + if (!affectedOrgUserIds.Any()) + { + return; + } + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(dbContext, organizationId); + + var missingDefaultCollectionUserIds = affectedOrgUserIds.Except(orgUserIdWithDefaultCollection); + + var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); + + if (!collectionUsers.Any() || !collections.Any()) + { + return; + } + + await dbContext.BulkCopyAsync(collections); + await dbContext.BulkCopyAsync(collectionUsers); + + await dbContext.SaveChangesAsync(); + } + + private async Task> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId) + { + var results = await dbContext.OrganizationUsers + .Where(ou => ou.OrganizationId == organizationId) + .Join( + dbContext.CollectionUsers, + ou => ou.Id, + cu => cu.OrganizationUserId, + (ou, cu) => new { ou, cu } + ) + .Join( + dbContext.Collections, + temp => temp.cu.CollectionId, + c => c.Id, + (temp, c) => new { temp.ou, Collection = c } + ) + .Where(x => x.Collection.Type == CollectionType.DefaultUserCollection) + .Select(x => x.ou.Id) + .ToListAsync(); + + return results.ToHashSet(); + } + + private (List collectionUser, List collection) BuildDefaultCollectionForUsers(Guid organizationId, IEnumerable missingDefaultCollectionUserIds, string defaultCollectionName) + { + var collectionUsers = new List(); + var collections = new List(); + + foreach (var orgUserId in missingDefaultCollectionUserIds) + { + var collectionId = Guid.NewGuid(); + + collections.Add(new Collection + { + Id = collectionId, + OrganizationId = organizationId, + Name = defaultCollectionName, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = null + + }); + + collectionUsers.Add(new CollectionUser + { + CollectionId = collectionId, + OrganizationUserId = orgUserId, + ReadOnly = false, + HidePasswords = false, + Manage = true, + }); + } + + return (collectionUsers, collections); + } } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCollectionDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCollectionDetailsQuery.cs index 6e513e8098..14dd8c876c 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCollectionDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCollectionDetailsQuery.cs @@ -47,17 +47,18 @@ public class UserCollectionDetailsQuery : IQuery ((cu == null ? (Guid?)null : cu.CollectionId) != null || (cg == null ? (Guid?)null : cg.CollectionId) != null) select new { c, ou, o, cu, gu, g, cg }; - return query.Select(x => new CollectionDetails + return query.Select(row => new CollectionDetails { - Id = x.c.Id, - OrganizationId = x.c.OrganizationId, - Name = x.c.Name, - ExternalId = x.c.ExternalId, - CreationDate = x.c.CreationDate, - RevisionDate = x.c.RevisionDate, - ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false, - HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false, - Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false, + Id = row.c.Id, + OrganizationId = row.c.OrganizationId, + Name = row.c.Name, + ExternalId = row.c.ExternalId, + CreationDate = row.c.CreationDate, + RevisionDate = row.c.RevisionDate, + ReadOnly = (bool?)row.cu.ReadOnly ?? (bool?)row.cg.ReadOnly ?? false, + HidePasswords = (bool?)row.cu.HidePasswords ?? (bool?)row.cg.HidePasswords ?? false, + Manage = (bool?)row.cu.Manage ?? (bool?)row.cg.Manage ?? false, + Type = row.c.Type }); } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs new file mode 100644 index 0000000000..d85cc1e813 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs @@ -0,0 +1,154 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CreateDefaultCollectionsTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateDefaultCollectionsAsync_ShouldCreateDefaultCollection_WhenUsersDoNotHaveDefaultCollection( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var resultOrganizationUsers = await Task.WhenAll( + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization), + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) + ); + + + var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id); + var defaultCollectionName = $"default-name-{organization.Id}"; + + // Act + await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + + // Assert + await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); + + await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateDefaultCollectionsAsync_ShouldUpsertCreateDefaultCollection_ForUsersWithAndWithoutDefaultCollectionsExist( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var arrangedOrganizationUsers = await Task.WhenAll( + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization), + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) + ); + + var arrangedOrgUserIds = arrangedOrganizationUsers.Select(organizationUser => organizationUser.Id); + var defaultCollectionName = $"default-name-{organization.Id}"; + + + await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, arrangedOrgUserIds, defaultCollectionName, arrangedOrganizationUsers); + + var newOrganizationUsers = new List() + { + await CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) + }; + + var affectedOrgUsers = newOrganizationUsers.Concat(arrangedOrganizationUsers); + var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id); + + // Act + await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + + // Assert + await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, arrangedOrganizationUsers, organization.Id); + + await CleanupAsync(organizationRepository, userRepository, organization, affectedOrgUsers); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateDefaultCollectionsAsync_ShouldNotCreateDefaultCollection_WhenUsersAlreadyHaveOne( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var resultOrganizationUsers = await Task.WhenAll( + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization), + CreateUserForOrgAsync(userRepository, organizationUserRepository, organization) + ); + + var affectedOrgUserIds = resultOrganizationUsers.Select(organizationUser => organizationUser.Id); + var defaultCollectionName = $"default-name-{organization.Id}"; + + + await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers); + + // Act + await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + + // Assert + await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); + + await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers); + } + + private static async Task CreateUsersWithExistingDefaultCollectionsAsync(ICollectionRepository collectionRepository, + Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName, + OrganizationUser[] resultOrganizationUsers) + { + await collectionRepository.CreateDefaultCollectionsAsync(organizationId, affectedOrgUserIds, defaultCollectionName); + + await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organizationId); + } + + private static async Task AssertAllUsersHaveOneDefaultCollectionAsync(ICollectionRepository collectionRepository, + IEnumerable organizationUsers, Guid organizationId) + { + foreach (var organizationUser in organizationUsers) + { + var collectionDetails = await collectionRepository.GetManyByUserIdAsync(organizationUser!.UserId.Value); + var defaultCollection = collectionDetails + .SingleOrDefault(collectionDetail => + collectionDetail.OrganizationId == organizationId + && collectionDetail.Type == CollectionType.DefaultUserCollection); + + Assert.NotNull(defaultCollection); + } + } + + private static async Task CreateUserForOrgAsync(IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, Organization organization) + { + + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + return orgUser; + } + + private static async Task CleanupAsync(IOrganizationRepository organizationRepository, + IUserRepository userRepository, + Organization organization, + IEnumerable organizationUsers) + { + await organizationRepository.DeleteAsync(organization); + + await userRepository.DeleteManyAsync( + organizationUsers + .Where(organizationUser => organizationUser.UserId != null) + .Select(organizationUser => new User() { Id = organizationUser.UserId.Value }) + ); + } +} From de13932ffe52d09f5603e7868ff067382ddf7c6c Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 31 Jul 2025 11:24:39 -0400 Subject: [PATCH 115/326] [PM-22108] Add PolicyDetails_ReadByOrganizationId proc (#6019) --- .../Policies/OrganizationPolicyDetails.cs | 6 + .../Repositories/IPolicyRepository.cs | 13 + .../Repositories/PolicyRepository.cs | 13 + .../Repositories/PolicyRepository.cs | 89 ++++++ .../PolicyDetails_ReadByOrganizationId.sql | 81 ++++++ src/Sql/dbo/Tables/OrganizationUser.sql | 9 +- src/Sql/dbo/Tables/ProviderOrganization.sql | 4 + src/Sql/dbo/Tables/ProviderUser.sql | 5 + ...PolicyDetailsByOrganizationIdAsyncTests.cs | 258 ++++++++++++++++++ ..._00_PolicyDetails_ReadByOrganizationId.sql | 81 ++++++ .../DbScripts/2025-07-18_00_AddIndices.sql | 34 +++ 11 files changed, 589 insertions(+), 4 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs create mode 100644 src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs create mode 100644 util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql diff --git a/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs new file mode 100644 index 0000000000..eab0c9456f --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/Policies/OrganizationPolicyDetails.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies; + +public class OrganizationPolicyDetails : PolicyDetails +{ + public Guid UserId { get; set; } +} diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 4c0c03536d..2b46c040bb 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -31,4 +31,17 @@ public interface IPolicyRepository : IRepository /// You probably do not want to call it directly. /// Task> GetPolicyDetailsByUserId(Guid userId); + + /// + /// Retrieves of the specified + /// for users in the given organization and for any other organizations those users belong to. + /// + /// + /// 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> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index 071ff3153a..c93c66c94d 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -73,4 +73,17 @@ public class PolicyRepository : Repository, IPolicyRepository return results.ToList(); } } + + public async Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[PolicyDetails_ReadByOrganizationId]", + new { @OrganizationId = organizationId, @PolicyType = policyType }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs index f9287a20a9..9d25fd5541 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -94,4 +94,93 @@ public class PolicyRepository : Repository> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var givenOrgUsers = + from ou in dbContext.OrganizationUsers + where ou.OrganizationId == organizationId + from u in dbContext.Users + where + (u.Email == ou.Email && ou.Email != null) + || (ou.UserId == u.Id && ou.UserId != null) + + select new + { + ou.Id, + ou.OrganizationId, + UserId = u.Id, + u.Email + }; + + var orgUsersLinkedByUserId = + from ou in dbContext.OrganizationUsers + join gou in givenOrgUsers + on ou.UserId equals gou.UserId + select new + { + ou.Id, + ou.OrganizationId, + gou.UserId, + ou.Type, + ou.Status, + ou.Permissions + }; + + var orgUsersLinkedByEmail = + from ou in dbContext.OrganizationUsers + join gou in givenOrgUsers + on ou.Email equals gou.Email + select new + { + ou.Id, + ou.OrganizationId, + gou.UserId, + ou.Type, + ou.Status, + ou.Permissions + }; + + var allAffectedOrgUsers = orgUsersLinkedByEmail.Union(orgUsersLinkedByUserId); + + var providerOrganizations = from pu in dbContext.ProviderUsers + join po in dbContext.ProviderOrganizations + on pu.ProviderId equals po.ProviderId + join ou in allAffectedOrgUsers + on pu.UserId equals ou.UserId + where pu.UserId == ou.UserId + select new + { + pu.UserId, + po.OrganizationId + }; + + var policyWithAffectedUsers = + from p in dbContext.Policies + join o in dbContext.Organizations + on p.OrganizationId equals o.Id + join ou in allAffectedOrgUsers + on o.Id equals ou.OrganizationId + where p.Enabled + && o.Enabled + && o.UsePolicies + && p.Type == policyType + select new OrganizationPolicyDetails + { + UserId = ou.UserId, + 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 policyWithAffectedUsers.ToListAsync(); + } } diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql new file mode 100644 index 0000000000..526a9141ac --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql @@ -0,0 +1,81 @@ +CREATE PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON; + + -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email. + ;WITH GivenOrgUsers AS ( + SELECT + OU.[UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId] + WHERE OU.[OrganizationId] = @OrganizationId + + UNION ALL + + SELECT + U.[Id] AS [UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] + WHERE OU.[OrganizationId] = @OrganizationId + ), + + -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers. + AllOrgUsers AS ( + SELECT + OU.[Id] AS [OrganizationUserId], + OU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId] + UNION ALL + SELECT + OU.[Id] AS [OrganizationUserId], + AU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email] + ) + + -- Return policy details for each matching organization user. + SELECT + OU.[OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Data] AS [PolicyData], + OU.[OrganizationUserType], + OU.[OrganizationUserStatus], + OU.[OrganizationUserPermissionsData], + -- Check if user is a provider for the organization + 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].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id] + WHERE P.[Enabled] = 1 + AND O.[Enabled] = 1 + AND O.[UsePolicies] = 1 + AND P.[Type] = @PolicyType + +END +GO diff --git a/src/Sql/dbo/Tables/OrganizationUser.sql b/src/Sql/dbo/Tables/OrganizationUser.sql index 513a5f6696..51ed2115bc 100644 --- a/src/Sql/dbo/Tables/OrganizationUser.sql +++ b/src/Sql/dbo/Tables/OrganizationUser.sql @@ -17,20 +17,21 @@ CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); - GO CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserIdOrganizationIdStatusV2] ON [dbo].[OrganizationUser]([UserId] ASC, [OrganizationId] ASC, [Status] ASC); - - GO + CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId] ON [dbo].[OrganizationUser]([OrganizationId] ASC); +GO +CREATE NONCLUSTERED INDEX IX_OrganizationUser_EmailOrganizationIdStatus + ON OrganizationUser (Email ASC, OrganizationId ASC, [Status] ASC); GO CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId] ON [dbo].[OrganizationUser] ([OrganizationId], [UserId]) - INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], + INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]); GO diff --git a/src/Sql/dbo/Tables/ProviderOrganization.sql b/src/Sql/dbo/Tables/ProviderOrganization.sql index ccf5455ab3..e6a7dd9270 100644 --- a/src/Sql/dbo/Tables/ProviderOrganization.sql +++ b/src/Sql/dbo/Tables/ProviderOrganization.sql @@ -10,3 +10,7 @@ CONSTRAINT [FK_ProviderOrganization_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_ProviderOrganization_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ); + +GO +CREATE NONCLUSTERED INDEX IX_ProviderOrganization_OrganizationIdProviderId + ON [dbo].[ProviderOrganization] ([OrganizationId], [ProviderId]); diff --git a/src/Sql/dbo/Tables/ProviderUser.sql b/src/Sql/dbo/Tables/ProviderUser.sql index 8905242aa9..b18b4a4afe 100644 --- a/src/Sql/dbo/Tables/ProviderUser.sql +++ b/src/Sql/dbo/Tables/ProviderUser.sql @@ -13,3 +13,8 @@ CONSTRAINT [FK_ProviderUser_Provider] FOREIGN KEY ([ProviderId]) REFERENCES [dbo].[Provider] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_ProviderUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); + + +GO +CREATE NONCLUSTERED INDEX IX_ProviderUser_UserIdProviderId + ON [dbo].[ProviderUser] ([UserId], [ProviderId]); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs new file mode 100644 index 0000000000..7dc4b6d2b3 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs @@ -0,0 +1,258 @@ +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.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; + +public class GetPolicyDetailsByOrganizationIdAsyncTests +{ + [DatabaseTheory, DatabaseData] + public async Task ShouldContainProviderData( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + await ArrangeProvider(); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + Assert.Single(results); + + Assert.True(results.Single().IsProvider); + + async Task ArrangeProvider() + { + 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 = userOrgConnectedDirectly.OrganizationId, + ProviderId = provider.Id + }); + } + } + + [DatabaseTheory, DatabaseData] + public async Task ShouldNotReturnOtherOrganizations_WhenUserIsNotConnected( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + const PolicyType policyType = PolicyType.SingleOrg; + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var notConnectedOrg = await CreateEnterpriseOrg(organizationRepository); + await policyRepository.CreateAsync(new Policy { OrganizationId = notConnectedOrg.Id, Enabled = true, Type = policyType }); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, PolicyType.SingleOrg)).ToList(); + + // Assert + Assert.Single(results); + + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedDirectly.Id + && result.OrganizationId == userOrgConnectedDirectly.OrganizationId); + Assert.DoesNotContain(results, result => result.OrganizationId == notConnectedOrg.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task ShouldOnlyReturnInputPolicyType( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + const PolicyType inputPolicyType = PolicyType.SingleOrg; + var orgUser = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, inputPolicyType); + + const PolicyType notInputPolicyType = PolicyType.RequireSso; + await policyRepository.CreateAsync(new Policy { OrganizationId = orgUser.OrganizationId, Enabled = true, Type = notInputPolicyType }); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(orgUser.OrganizationId, inputPolicyType)).ToList(); + + // Assert + Assert.Single(results); + + Assert.Contains(results, result => result.OrganizationUserId == orgUser.Id + && result.OrganizationId == orgUser.OrganizationId + && result.PolicyType == inputPolicyType); + + Assert.DoesNotContain(results, result => result.PolicyType == notInputPolicyType); + } + + + [DatabaseTheory, DatabaseData] + public async Task WhenDirectlyConnectedUserHasUserId_ShouldReturnOtherConnectedOrganizationPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByEmail = await ArrangeOtherOrgConnectedByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByUserId = await ArrangeOtherOrgConnectedByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + const int expectedCount = 3; + Assert.Equal(expectedCount, results.Count); + + AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + } + + [DatabaseTheory, DatabaseData] + public async Task WhenDirectlyConnectedUserHasEmail_ShouldReturnOtherConnectedOrganizationPolicies( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var userOrgConnectedDirectly = await ArrangeDirectlyConnectedOrgByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByEmail = await ArrangeOtherOrgConnectedByEmailAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + var userOrgConnectedByUserId = await ArrangeOtherOrgConnectedByUserIdAsync(organizationUserRepository, organizationRepository, policyRepository, user, policyType); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(userOrgConnectedDirectly.OrganizationId, policyType)).ToList(); + + // Assert + AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + } + + private async Task ArrangeOtherOrgConnectedByUserIdAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private async Task ArrangeDirectlyConnectedOrgByUserIdAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + + var organizationUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private static void AssertPolicyDetailUserConnections(List results, + OrganizationUser userOrgConnectedDirectly, + OrganizationUser userOrgConnectedByEmail, + OrganizationUser userOrgConnectedByUserId) + { + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedDirectly.Id + && result.OrganizationId == userOrgConnectedDirectly.OrganizationId); + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedByEmail.Id + && result.OrganizationId == userOrgConnectedByEmail.OrganizationId); + Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedByUserId.Id + && result.OrganizationId == userOrgConnectedByUserId.OrganizationId); + } + + private async Task ArrangeOtherOrgConnectedByEmailAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + var organizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Custom, + Email = user.Email + }; + await organizationUserRepository.CreateAsync(organizationUser); + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private async Task ArrangeDirectlyConnectedOrgByEmailAsync(IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, + PolicyType policyType) + { + var organization = await CreateEnterpriseOrg(organizationRepository); + var organizationUser = new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Custom, + Email = user.Email + }; + await organizationUserRepository.CreateAsync(organizationUser); + + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + return organizationUser; + } + + private Task CreateEnterpriseOrg(IOrganizationRepository orgRepo) + => orgRepo.CreateAsync(new Organization + { + Name = System.Guid.NewGuid().ToString(), + BillingEmail = "billing@example.com", + Plan = "Test", + PlanType = PlanType.EnterpriseAnnually, + UsePolicies = true + }); +} diff --git a/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql b/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql new file mode 100644 index 0000000000..a318d0af26 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-17_00_PolicyDetails_ReadByOrganizationId.sql @@ -0,0 +1,81 @@ +CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON; + + -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email. + ;WITH GivenOrgUsers AS ( + SELECT + OU.[UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId] + WHERE OU.[OrganizationId] = @OrganizationId + + UNION ALL + + SELECT + U.[Id] AS [UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] + WHERE OU.[OrganizationId] = @OrganizationId + ), + + -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers. + AllOrgUsers AS ( + SELECT + OU.[Id] AS [OrganizationUserId], + OU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId] + UNION ALL + SELECT + OU.[Id] AS [OrganizationUserId], + AU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email] + ) + + -- Return policy details for each matching organization user. + SELECT + OU.[OrganizationUserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Data] AS [PolicyData], + OU.[OrganizationUserType], + OU.[OrganizationUserStatus], + OU.[OrganizationUserPermissionsData], + -- Check if user is a provider for the organization + 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].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id] + WHERE P.[Enabled] = 1 + AND O.[Enabled] = 1 + AND O.[UsePolicies] = 1 + AND P.[Type] = @PolicyType + +END +GO diff --git a/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql b/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql new file mode 100644 index 0000000000..4082082324 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-18_00_AddIndices.sql @@ -0,0 +1,34 @@ +-- Adding indices +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = 'IX_OrganizationUser_EmailOrganizationIdStatus' + AND object_id = Object_id('[dbo].[OrganizationUser]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationUser_EmailOrganizationIdStatus] + ON [dbo].[OrganizationUser]([email] ASC, [organizationid] ASC, [status] ASC) + END + +go + +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = + 'IX_ProviderOrganization_OrganizationIdProviderId' + AND object_id = Object_id('[dbo].[ProviderOrganization]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_ProviderOrganization_OrganizationIdProviderId] + ON [dbo].[ProviderOrganization]([organizationid] ASC, [providerid] ASC) + END + +go + +IF NOT EXISTS (SELECT * + FROM sys.indexes + WHERE [name] = 'IX_ProviderUser_UserIdProviderId' + AND object_id = Object_id('[dbo].[ProviderUser]')) + BEGIN + CREATE NONCLUSTERED INDEX [IX_ProviderUser_UserIdProviderId] + ON [dbo].[ProviderUser]([userid] ASC, [providerid] ASC) + END + +go From 6f4a0c4a617fa1a06a50db8df851cec0529e50a9 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Thu, 31 Jul 2025 11:27:53 -0400 Subject: [PATCH 116/326] [PM-15052] Add RevokeOrganizationUserCommand (#6111) --- .../Scim/Controllers/v2/UsersController.cs | 14 +- .../src/Scim/Users/PatchUserCommand.cs | 17 ++- .../Scim.Test/Users/PatchUserCommandTests.cs | 8 +- .../OrganizationUsersController.cs | 16 +-- .../IRevokeOrganizationUserCommand.cs | 12 ++ .../RevokeOrganizationUserCommand.cs | 135 ++++++++++++++++++ .../Services/IOrganizationService.cs | 4 - .../Implementations/OrganizationService.cs | 132 ----------------- ...OrganizationServiceCollectionExtensions.cs | 1 + .../RevokeOrganizationUserCommandTests.cs | 83 +++++++++++ .../Services/OrganizationServiceTests.cs | 57 -------- 11 files changed, 256 insertions(+), 223 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeOrganizationUserCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommandTests.cs diff --git a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs index 4d292281dd..afbfa50bb4 100644 --- a/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs +++ b/bitwarden_license/src/Scim/Controllers/v2/UsersController.cs @@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Scim.Models; using Bit.Scim.Users.Interfaces; using Bit.Scim.Utilities; @@ -22,29 +21,28 @@ namespace Bit.Scim.Controllers.v2; public class UsersController : Controller { private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IOrganizationService _organizationService; private readonly IGetUsersListQuery _getUsersListQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IPatchUserCommand _patchUserCommand; private readonly IPostUserCommand _postUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; + private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand; - public UsersController( - IOrganizationUserRepository organizationUserRepository, - IOrganizationService organizationService, + public UsersController(IOrganizationUserRepository organizationUserRepository, IGetUsersListQuery getUsersListQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IPatchUserCommand patchUserCommand, IPostUserCommand postUserCommand, - IRestoreOrganizationUserCommand restoreOrganizationUserCommand) + IRestoreOrganizationUserCommand restoreOrganizationUserCommand, + IRevokeOrganizationUserCommand revokeOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; - _organizationService = organizationService; _getUsersListQuery = getUsersListQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _patchUserCommand = patchUserCommand; _postUserCommand = postUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; + _revokeOrganizationUserCommand = revokeOrganizationUserCommand; } [HttpGet("{id}")] @@ -101,7 +99,7 @@ public class UsersController : Controller } else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked) { - await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM); + await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM); } // Have to get full details object for response model diff --git a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs index 3d7082aacc..6c983611ee 100644 --- a/bitwarden_license/src/Scim/Users/PatchUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PatchUserCommand.cs @@ -1,8 +1,8 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Scim.Models; using Bit.Scim.Users.Interfaces; @@ -11,20 +11,19 @@ namespace Bit.Scim.Users; public class PatchUserCommand : IPatchUserCommand { private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IOrganizationService _organizationService; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly ILogger _logger; + private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand; - public PatchUserCommand( - IOrganizationUserRepository organizationUserRepository, - IOrganizationService organizationService, + public PatchUserCommand(IOrganizationUserRepository organizationUserRepository, IRestoreOrganizationUserCommand restoreOrganizationUserCommand, - ILogger logger) + ILogger logger, + IRevokeOrganizationUserCommand revokeOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; - _organizationService = organizationService; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; _logger = logger; + _revokeOrganizationUserCommand = revokeOrganizationUserCommand; } public async Task PatchUserAsync(Guid organizationId, Guid id, ScimPatchModel model) @@ -80,7 +79,7 @@ public class PatchUserCommand : IPatchUserCommand } else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked) { - await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM); + await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM); return true; } return false; diff --git a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs index 44a43d16b7..f391c93fe3 100644 --- a/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs +++ b/bitwarden_license/test/Scim.Test/Users/PatchUserCommandTests.cs @@ -1,10 +1,10 @@ using System.Text.Json; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Scim.Models; using Bit.Scim.Users; using Bit.Scim.Utilities; @@ -101,7 +101,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM); + await sutProvider.GetDependency().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -129,7 +129,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); - await sutProvider.GetDependency().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM); + await sutProvider.GetDependency().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM); } [Theory] @@ -149,7 +149,7 @@ public class PatchUserCommandTests await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM); } [Theory] diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 55f1c9de14..3365e754ca 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -18,7 +18,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Enums; @@ -57,7 +56,6 @@ public class OrganizationUsersController : Controller private readonly IApplicationCacheService _applicationCacheService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand; private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; @@ -67,9 +65,9 @@ public class OrganizationUsersController : Controller private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; + private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand; - public OrganizationUsersController( - IOrganizationRepository organizationRepository, + public OrganizationUsersController(IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, ICollectionRepository collectionRepository, @@ -85,7 +83,6 @@ public class OrganizationUsersController : Controller IApplicationCacheService applicationCacheService, ISsoConfigRepository ssoConfigRepository, IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand, IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, @@ -94,7 +91,8 @@ public class OrganizationUsersController : Controller IPricingClient pricingClient, IConfirmOrganizationUserCommand confirmOrganizationUserCommand, IRestoreOrganizationUserCommand restoreOrganizationUserCommand, - IInitPendingOrganizationCommand initPendingOrganizationCommand) + IInitPendingOrganizationCommand initPendingOrganizationCommand, + IRevokeOrganizationUserCommand revokeOrganizationUserCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -112,7 +110,6 @@ public class OrganizationUsersController : Controller _applicationCacheService = applicationCacheService; _ssoConfigRepository = ssoConfigRepository; _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand; _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; @@ -122,6 +119,7 @@ public class OrganizationUsersController : Controller _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; _initPendingOrganizationCommand = initPendingOrganizationCommand; + _revokeOrganizationUserCommand = revokeOrganizationUserCommand; } [HttpGet("{id}")] @@ -545,7 +543,7 @@ public class OrganizationUsersController : Controller [Authorize] public async Task RevokeAsync(Guid orgId, Guid id) { - await RestoreOrRevokeUserAsync(orgId, id, _organizationService.RevokeUserAsync); + await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync); } [HttpPatch("revoke")] @@ -553,7 +551,7 @@ public class OrganizationUsersController : Controller [Authorize] public async Task> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - return await RestoreOrRevokeUsersAsync(orgId, model, _organizationService.RevokeUsersAsync); + return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync); } [HttpPatch("{id}/restore")] diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeOrganizationUserCommand.cs new file mode 100644 index 0000000000..01ad2f05d2 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeOrganizationUserCommand.cs @@ -0,0 +1,12 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IRevokeOrganizationUserCommand +{ + Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); + Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); + Task>> RevokeUsersAsync(Guid organizationId, + IEnumerable organizationUserIds, Guid? revokingUserId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommand.cs new file mode 100644 index 0000000000..f24e0ae265 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommand.cs @@ -0,0 +1,135 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class RevokeOrganizationUserCommand( + IEventService eventService, + IPushNotificationService pushNotificationService, + IOrganizationUserRepository organizationUserRepository, + ICurrentContext currentContext, + IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery) + : IRevokeOrganizationUserCommand +{ + public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId) + { + if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value) + { + throw new BadRequestException("You cannot revoke yourself."); + } + + if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && + !await currentContext.OrganizationOwner(organizationUser.OrganizationId)) + { + throw new BadRequestException("Only owners can revoke other owners."); + } + + await RepositoryRevokeUserAsync(organizationUser); + await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + + if (organizationUser.UserId.HasValue) + { + await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } + } + + public async Task RevokeUserAsync(OrganizationUser organizationUser, + EventSystemUser systemUser) + { + await RepositoryRevokeUserAsync(organizationUser); + await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, + systemUser); + + if (organizationUser.UserId.HasValue) + { + await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } + } + + private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser) + { + if (organizationUser.Status == OrganizationUserStatusType.Revoked) + { + throw new BadRequestException("Already revoked."); + } + + if (!await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, + new[] { organizationUser.Id }, includeProvider: true)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + + await organizationUserRepository.RevokeAsync(organizationUser.Id); + organizationUser.Status = OrganizationUserStatusType.Revoked; + } + + public async Task>> RevokeUsersAsync(Guid organizationId, + IEnumerable organizationUserIds, Guid? revokingUserId) + { + var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds); + var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) + .ToList(); + + if (!filteredUsers.Any()) + { + throw new BadRequestException("Users invalid."); + } + + if (!await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) + { + throw new BadRequestException("Organization must have at least one confirmed owner."); + } + + var deletingUserIsOwner = false; + if (revokingUserId.HasValue) + { + deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId); + } + + var result = new List>(); + + foreach (var organizationUser in filteredUsers) + { + try + { + if (organizationUser.Status == OrganizationUserStatusType.Revoked) + { + throw new BadRequestException("Already revoked."); + } + + if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId) + { + throw new BadRequestException("You cannot revoke yourself."); + } + + if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && + !deletingUserIsOwner) + { + throw new BadRequestException("Only owners can revoke other owners."); + } + + await organizationUserRepository.RevokeAsync(organizationUser.Id); + organizationUser.Status = OrganizationUserStatusType.Revoked; + await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + if (organizationUser.UserId.HasValue) + { + await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); + } + + result.Add(Tuple.Create(organizationUser, "")); + } + catch (BadRequestException e) + { + result.Add(Tuple.Create(organizationUser, e.Message)); + } + } + + return result; + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index bec9507adf..05c84c731c 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -35,10 +35,6 @@ public interface IOrganizationService IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting, EventSystemUser eventSystemUser); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); - Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); - Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser); - Task>> RevokeUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? revokingUserId); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 84fb9532dc..b1a07338a3 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -21,7 +21,6 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -44,13 +43,9 @@ public class OrganizationService : IOrganizationService { private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly ICollectionRepository _collectionRepository; private readonly IGroupRepository _groupRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushNotificationService; - private readonly IPushRegistrationService _pushRegistrationService; - private readonly IDeviceRepository _deviceRepository; - private readonly ILicensingService _licensingService; private readonly IEventService _eventService; private readonly IApplicationCacheService _applicationCacheService; private readonly IPaymentService _paymentService; @@ -58,7 +53,6 @@ public class OrganizationService : IOrganizationService private readonly IPolicyService _policyService; private readonly ISsoUserRepository _ssoUserRepository; private readonly IGlobalSettings _globalSettings; - private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; private readonly IProviderOrganizationRepository _providerOrganizationRepository; @@ -75,13 +69,9 @@ public class OrganizationService : IOrganizationService public OrganizationService( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, - ICollectionRepository collectionRepository, IGroupRepository groupRepository, IMailService mailService, IPushNotificationService pushNotificationService, - IPushRegistrationService pushRegistrationService, - IDeviceRepository deviceRepository, - ILicensingService licensingService, IEventService eventService, IApplicationCacheService applicationCacheService, IPaymentService paymentService, @@ -89,7 +79,6 @@ public class OrganizationService : IOrganizationService IPolicyService policyService, ISsoUserRepository ssoUserRepository, IGlobalSettings globalSettings, - IOrganizationApiKeyRepository organizationApiKeyRepository, ICurrentContext currentContext, ILogger logger, IProviderOrganizationRepository providerOrganizationRepository, @@ -106,13 +95,9 @@ public class OrganizationService : IOrganizationService { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; - _collectionRepository = collectionRepository; _groupRepository = groupRepository; _mailService = mailService; _pushNotificationService = pushNotificationService; - _pushRegistrationService = pushRegistrationService; - _deviceRepository = deviceRepository; - _licensingService = licensingService; _eventService = eventService; _applicationCacheService = applicationCacheService; _paymentService = paymentService; @@ -120,7 +105,6 @@ public class OrganizationService : IOrganizationService _policyService = policyService; _ssoUserRepository = ssoUserRepository; _globalSettings = globalSettings; - _organizationApiKeyRepository = organizationApiKeyRepository; _currentContext = currentContext; _logger = logger; _providerOrganizationRepository = providerOrganizationRepository; @@ -1453,122 +1437,6 @@ public class OrganizationService : IOrganizationService return true; } - public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId) - { - if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value) - { - throw new BadRequestException("You cannot revoke yourself."); - } - - if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && - !await _currentContext.OrganizationOwner(organizationUser.OrganizationId)) - { - throw new BadRequestException("Only owners can revoke other owners."); - } - - await RepositoryRevokeUserAsync(organizationUser); - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); - - if (organizationUser.UserId.HasValue) - { - await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); - } - } - - public async Task RevokeUserAsync(OrganizationUser organizationUser, - EventSystemUser systemUser) - { - await RepositoryRevokeUserAsync(organizationUser); - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, - systemUser); - - if (organizationUser.UserId.HasValue) - { - await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); - } - } - - private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser) - { - if (organizationUser.Status == OrganizationUserStatusType.Revoked) - { - throw new BadRequestException("Already revoked."); - } - - if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, - new[] { organizationUser.Id }, includeProvider: true)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - await _organizationUserRepository.RevokeAsync(organizationUser.Id); - organizationUser.Status = OrganizationUserStatusType.Revoked; - } - - public async Task>> RevokeUsersAsync(Guid organizationId, - IEnumerable organizationUserIds, Guid? revokingUserId) - { - var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds); - var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) - .ToList(); - - if (!filteredUsers.Any()) - { - throw new BadRequestException("Users invalid."); - } - - if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds)) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - - var deletingUserIsOwner = false; - if (revokingUserId.HasValue) - { - deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); - } - - var result = new List>(); - - foreach (var organizationUser in filteredUsers) - { - try - { - if (organizationUser.Status == OrganizationUserStatusType.Revoked) - { - throw new BadRequestException("Already revoked."); - } - - if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId) - { - throw new BadRequestException("You cannot revoke yourself."); - } - - if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && - !deletingUserIsOwner) - { - throw new BadRequestException("Only owners can revoke other owners."); - } - - await _organizationUserRepository.RevokeAsync(organizationUser.Id); - organizationUser.Status = OrganizationUserStatusType.Revoked; - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); - if (organizationUser.UserId.HasValue) - { - await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value); - } - - result.Add(Tuple.Create(organizationUser, "")); - } - catch (BadRequestException e) - { - result.Add(Tuple.Create(organizationUser, e.Message)); - } - } - - return result; - } - public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser) { // Determine status to revert back to diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index e28831e0ab..998354b9f8 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -127,6 +127,7 @@ public static class OrganizationServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommandTests.cs new file mode 100644 index 0000000000..b16a80d7a2 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeOrganizationUserCommandTests.cs @@ -0,0 +1,83 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; + +[SutProviderCustomize] +public class RevokeOrganizationUserCommandTests +{ + + [Theory, BitAutoData] + public async Task RevokeUser_Success( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser] OrganizationUser organizationUser, + SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); + + await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id); + + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + + [Theory, BitAutoData] + public async Task RevokeUser_WithEventSystemUser_Success( + Organization organization, + [OrganizationUser] OrganizationUser organizationUser, + EventSystemUser eventSystemUser, + SutProvider sutProvider) + { + RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); + + await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser); + + await sutProvider.GetDependency() + .Received(1) + .RevokeAsync(organizationUser.Id); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser); + await sutProvider.GetDependency() + .Received(1) + .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); + } + + private void RestoreRevokeUser_Setup( + Organization organization, + OrganizationUser? requestingOrganizationUser, + OrganizationUser targetOrganizationUser, + SutProvider sutProvider) + { + if (requestingOrganizationUser != null) + { + requestingOrganizationUser.OrganizationId = organization.Id; + } + targetOrganizationUser.OrganizationId = organization.Id; + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); + sutProvider.GetDependency() + .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) + .Returns(true); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 3271ea559b..923eaae871 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -15,7 +15,6 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; -using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -961,62 +960,6 @@ public class OrganizationServiceTests Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage); } - private void RestoreRevokeUser_Setup( - Organization organization, - OrganizationUser? requestingOrganizationUser, - OrganizationUser targetOrganizationUser, - SutProvider sutProvider) - { - if (requestingOrganizationUser != null) - { - requestingOrganizationUser.OrganizationId = organization.Id; - } - targetOrganizationUser.OrganizationId = organization.Id; - - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner); - sutProvider.GetDependency().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin)); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(organization.Id, Arg.Any>()) - .Returns(true); - } - - [Theory, BitAutoData] - public async Task RevokeUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser] OrganizationUser organizationUser, SutProvider sutProvider) - { - RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider); - - await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id); - - await sutProvider.GetDependency() - .Received(1) - .RevokeAsync(organizationUser.Id); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked); - await sutProvider.GetDependency() - .Received(1) - .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); - } - - [Theory, BitAutoData] - public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider sutProvider) - { - RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider); - - await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser); - - await sutProvider.GetDependency() - .Received(1) - .RevokeAsync(organizationUser.Id); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser); - await sutProvider.GetDependency() - .Received(1) - .PushSyncOrgKeysAsync(organizationUser.UserId!.Value); - } [Theory] [BitAutoData(PlanType.TeamsAnnually)] From ccedefb8b8d63819f789fee3d766b57a7006aa46 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Fri, 1 Aug 2025 08:46:00 -0400 Subject: [PATCH 117/326] [PM-17562] Update logs to use custom categories (#6145) * [PM-17562] Update logs to use custom categories * Added tests to verify hardcoded names match the real type --- .../Services/EventLoggingListenerService.cs | 4 +- .../AzureServiceBusEventListenerService.cs | 9 ++- ...ureServiceBusIntegrationListenerService.cs | 7 ++- .../RabbitMqEventListenerService.cs | 9 ++- .../RabbitMqIntegrationListenerService.cs | 7 ++- .../Utilities/ServiceCollectionExtensions.cs | 12 ++-- ...zureServiceBusEventListenerServiceTests.cs | 45 +++++++++---- ...rviceBusIntegrationListenerServiceTests.cs | 57 ++++++++++++++--- .../RabbitMqEventListenerServiceTests.cs | 39 +++++++++--- ...RabbitMqIntegrationListenerServiceTests.cs | 63 +++++++++++++++---- 10 files changed, 192 insertions(+), 60 deletions(-) diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs index ec2db121db..53ff3d4d0a 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs @@ -10,9 +10,9 @@ namespace Bit.Core.Services; public abstract class EventLoggingListenerService : BackgroundService { protected readonly IEventMessageHandler _handler; - protected ILogger _logger; + protected ILogger _logger; - protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger) + protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger) { _handler = handler; _logger = logger; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs index a4b83b8806..f5eb41c051 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs @@ -16,7 +16,8 @@ public class AzureServiceBusEventListenerService : EventLoggingL TConfiguration configuration, IEventMessageHandler handler, IAzureServiceBusService serviceBusService, - ILogger> logger) : base(handler, logger) + ILoggerFactory loggerFactory) + : base(handler, CreateLogger(loggerFactory, configuration)) { _processor = serviceBusService.CreateProcessor( topicName: configuration.EventTopicName, @@ -39,6 +40,12 @@ public class AzureServiceBusEventListenerService : EventLoggingL await base.StopAsync(cancellationToken); } + private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) + { + return loggerFactory.CreateLogger( + categoryName: $"Bit.Core.Services.AzureServiceBusEventListenerService.{configuration.EventSubscriptionName}"); + } + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) { _logger.LogError( diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs index 6db811efd9..037ae7e647 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs @@ -14,16 +14,17 @@ public class AzureServiceBusIntegrationListenerService : Backgro private readonly IAzureServiceBusService _serviceBusService; private readonly IIntegrationHandler _handler; private readonly ServiceBusProcessor _processor; - private readonly ILogger> _logger; + private readonly ILogger _logger; public AzureServiceBusIntegrationListenerService( TConfiguration configuration, IIntegrationHandler handler, IAzureServiceBusService serviceBusService, - ILogger> logger) + ILoggerFactory loggerFactory) { _handler = handler; - _logger = logger; + _logger = loggerFactory.CreateLogger( + categoryName: $"Bit.Core.Services.AzureServiceBusIntegrationListenerService.{configuration.IntegrationSubscriptionName}"); _maxRetries = configuration.MaxRetries; _serviceBusService = serviceBusService; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs index 09ce4ce767..5b089b06a6 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs @@ -19,7 +19,8 @@ public class RabbitMqEventListenerService : EventLoggingListener IEventMessageHandler handler, TConfiguration configuration, IRabbitMqService rabbitMqService, - ILogger> logger) : base(handler, logger) + ILoggerFactory loggerFactory) + : base(handler, CreateLogger(loggerFactory, configuration)) { _queueName = configuration.EventQueueName; _rabbitMqService = rabbitMqService; @@ -66,4 +67,10 @@ public class RabbitMqEventListenerService : EventLoggingListener } base.Dispose(); } + + private static ILogger CreateLogger(ILoggerFactory loggerFactory, TConfiguration configuration) + { + return loggerFactory.CreateLogger( + categoryName: $"Bit.Core.Services.RabbitMqEventListenerService.{configuration.EventQueueName}"); + } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index e8f368fbe5..59c8782985 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -20,14 +20,14 @@ public class RabbitMqIntegrationListenerService : BackgroundServ private readonly IIntegrationHandler _handler; private readonly Lazy> _lazyChannel; private readonly IRabbitMqService _rabbitMqService; - private readonly ILogger> _logger; + private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public RabbitMqIntegrationListenerService( IIntegrationHandler handler, TConfiguration configuration, IRabbitMqService rabbitMqService, - ILogger> logger, + ILoggerFactory loggerFactory, TimeProvider timeProvider) { _handler = handler; @@ -36,9 +36,10 @@ public class RabbitMqIntegrationListenerService : BackgroundServ _retryQueueName = configuration.IntegrationRetryQueueName; _queueName = configuration.IntegrationQueueName; _rabbitMqService = rabbitMqService; - _logger = logger; _timeProvider = timeProvider; _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); + _logger = loggerFactory.CreateLogger( + categoryName: $"Bit.Core.Services.RabbitMqIntegrationListenerService.{configuration.IntegrationQueueName}"); ; } public override async Task StartAsync(CancellationToken cancellationToken) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 48d3304ab0..c4e7009b4f 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -890,7 +890,7 @@ public static class ServiceCollectionExtensions configuration: listenerConfiguration, handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), serviceBusService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>() + loggerFactory: provider.GetRequiredService() ) ) ); @@ -900,7 +900,7 @@ public static class ServiceCollectionExtensions configuration: listenerConfiguration, handler: provider.GetRequiredService>(), serviceBusService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>() + loggerFactory: provider.GetRequiredService() ) ) ); @@ -941,7 +941,7 @@ public static class ServiceCollectionExtensions handler: provider.GetRequiredService(), configuration: repositoryConfiguration, rabbitMqService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>() + loggerFactory: provider.GetRequiredService() ) ) ); @@ -958,7 +958,7 @@ public static class ServiceCollectionExtensions configuration: repositoryConfiguration, handler: provider.GetRequiredService(), serviceBusService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>() + loggerFactory: provider.GetRequiredService() ) ) ); @@ -992,7 +992,7 @@ public static class ServiceCollectionExtensions handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey), configuration: listenerConfiguration, rabbitMqService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>() + loggerFactory: provider.GetRequiredService() ) ) ); @@ -1002,7 +1002,7 @@ public static class ServiceCollectionExtensions handler: provider.GetRequiredService>(), configuration: listenerConfiguration, rabbitMqService: provider.GetRequiredService(), - logger: provider.GetRequiredService>>(), + loggerFactory: provider.GetRequiredService(), timeProvider: provider.GetRequiredService() ) ) diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs index fb0adc2119..c6ef3063e2 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Azure.Messaging.ServiceBus; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Models.Data; @@ -17,14 +19,31 @@ public class AzureServiceBusEventListenerServiceTests { private const string _messageId = "messageId"; private readonly TestListenerConfiguration _config = new(); + private readonly ILogger _logger = Substitute.For(); private SutProvider> GetSutProvider() { + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger().ReturnsForAnyArgs(_logger); return new SutProvider>() .SetDependency(_config) + .SetDependency(loggerFactory) .Create(); } + [Fact] + public void Constructor_CreatesLogWithCorrectCategory() + { + var sutProvider = GetSutProvider(); + + var fullName = typeof(AzureServiceBusEventListenerService<>).FullName ?? ""; + var tickIndex = fullName.IndexOf('`'); + var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName; + var categoryName = cleanedName + '.' + _config.EventSubscriptionName; + + sutProvider.GetDependency().Received(1).CreateLogger(categoryName); + } + [Fact] public void Constructor_CreatesProcessor() { @@ -44,12 +63,12 @@ public class AzureServiceBusEventListenerServiceTests await sutProvider.Sut.ProcessErrorAsync(args); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -58,26 +77,26 @@ public class AzureServiceBusEventListenerServiceTests var sutProvider = GetSutProvider(); await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError() { var sutProvider = GetSutProvider(); - await sutProvider.Sut.ProcessReceivedMessageAsync("{ Inavlid JSON }", _messageId); + await sutProvider.Sut.ProcessReceivedMessageAsync("{ Invalid JSON }", _messageId); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), - Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Is(o => (o.ToString() ?? "").Contains("Invalid JSON")), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -89,12 +108,12 @@ public class AzureServiceBusEventListenerServiceTests _messageId ); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -106,12 +125,12 @@ public class AzureServiceBusEventListenerServiceTests _messageId ); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Theory, BitAutoData] diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs index f450863ebf..23627f3962 100644 --- a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Text.Json; using Azure.Messaging.ServiceBus; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; @@ -7,6 +8,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.Extensions.Logging; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; namespace Bit.Core.Test.Services; @@ -15,18 +17,35 @@ namespace Bit.Core.Test.Services; public class AzureServiceBusIntegrationListenerServiceTests { private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly ILogger _logger = Substitute.For(); private readonly IAzureServiceBusService _serviceBusService = Substitute.For(); private readonly TestListenerConfiguration _config = new(); private SutProvider> GetSutProvider() { + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger().ReturnsForAnyArgs(_logger); return new SutProvider>() .SetDependency(_config) + .SetDependency(loggerFactory) .SetDependency(_handler) .SetDependency(_serviceBusService) .Create(); } + [Fact] + public void Constructor_CreatesLogWithCorrectCategory() + { + var sutProvider = GetSutProvider(); + + var fullName = typeof(AzureServiceBusIntegrationListenerService<>).FullName ?? ""; + var tickIndex = fullName.IndexOf('`'); + var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName; + var categoryName = cleanedName + '.' + _config.IntegrationSubscriptionName; + + sutProvider.GetDependency().Received(1).CreateLogger(categoryName); + } + [Fact] public void Constructor_CreatesProcessor() { @@ -45,7 +64,7 @@ public class AzureServiceBusIntegrationListenerServiceTests var sutProvider = GetSutProvider(); await sutProvider.Sut.ProcessErrorAsync(args); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), @@ -63,12 +82,13 @@ public class AzureServiceBusIntegrationListenerServiceTests result.Retryable = false; _handler.HandleAsync(Arg.Any()).Returns(result); - var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); } [Theory, BitAutoData] @@ -81,12 +101,13 @@ public class AzureServiceBusIntegrationListenerServiceTests _handler.HandleAsync(Arg.Any()).Returns(result); - var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); } [Theory, BitAutoData] @@ -99,7 +120,8 @@ public class AzureServiceBusIntegrationListenerServiceTests result.Retryable = true; _handler.HandleAsync(Arg.Any()).Returns(result); - var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); @@ -114,11 +136,30 @@ public class AzureServiceBusIntegrationListenerServiceTests var result = new IntegrationHandlerResult(true, message); _handler.HandleAsync(Arg.Any()).Returns(result); - var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); - await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default!); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); + } + + [Fact] + public async Task HandleMessageAsync_UnknownError_LogsError() + { + var sutProvider = GetSutProvider(); + _handler.HandleAsync(Arg.Any()).ThrowsAsync(); + + Assert.True(await sutProvider.Sut.HandleMessageAsync("Bad JSON")); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(Arg.Any()); } } diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs index cf1d8f6a0e..22e297a00d 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Models.Data; using Bit.Core.Services; @@ -17,14 +19,31 @@ namespace Bit.Core.Test.Services; public class RabbitMqEventListenerServiceTests { private readonly TestListenerConfiguration _config = new(); + private readonly ILogger _logger = Substitute.For(); private SutProvider> GetSutProvider() { + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger().ReturnsForAnyArgs(_logger); return new SutProvider>() .SetDependency(_config) + .SetDependency(loggerFactory) .Create(); } + [Fact] + public void Constructor_CreatesLogWithCorrectCategory() + { + var sutProvider = GetSutProvider(); + + var fullName = typeof(RabbitMqEventListenerService<>).FullName ?? ""; + var tickIndex = fullName.IndexOf('`'); + var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName; + var categoryName = cleanedName + '.' + _config.EventQueueName; + + sutProvider.GetDependency().Received(1).CreateLogger(categoryName); + } + [Fact] public async Task StartAsync_CreatesQueue() { @@ -53,12 +72,12 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -76,12 +95,12 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), - Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Is(o => (o.ToString() ?? "").Contains("Invalid JSON")), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -99,12 +118,12 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Fact] @@ -122,12 +141,12 @@ public class RabbitMqEventListenerServiceTests await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); - sutProvider.GetDependency>>().Received(1).Log( + _logger.Received(1).Log( LogLevel.Error, Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any>()); + Arg.Any>()); } [Theory, BitAutoData] diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs index df40e17dd4..5fcd121252 100644 --- a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -1,9 +1,12 @@ -using System.Text; +#nullable enable + +using System.Text; 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.Extensions.Logging; using Microsoft.Extensions.Time.Testing; using NSubstitute; using RabbitMQ.Client; @@ -17,14 +20,18 @@ public class RabbitMqIntegrationListenerServiceTests { private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc); private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly ILogger _logger = Substitute.For(); private readonly IRabbitMqService _rabbitMqService = Substitute.For(); private readonly TestListenerConfiguration _config = new(); private SutProvider> GetSutProvider() { + var loggerFactory = Substitute.For(); + loggerFactory.CreateLogger().ReturnsForAnyArgs(_logger); var sutProvider = new SutProvider>() .SetDependency(_config) .SetDependency(_handler) + .SetDependency(loggerFactory) .SetDependency(_rabbitMqService) .WithFakeTimeProvider() .Create(); @@ -33,6 +40,19 @@ public class RabbitMqIntegrationListenerServiceTests return sutProvider; } + [Fact] + public void Constructor_CreatesLogWithCorrectCategory() + { + var sutProvider = GetSutProvider(); + + var fullName = typeof(RabbitMqIntegrationListenerService<>).FullName ?? ""; + var tickIndex = fullName.IndexOf('`'); + var cleanedName = tickIndex >= 0 ? fullName.Substring(0, tickIndex) : fullName; + var categoryName = cleanedName + '.' + _config.IntegrationQueueName; + + sutProvider.GetDependency().Received(1).CreateLogger(categoryName); + } + [Fact] public async Task StartAsync_CreatesQueues() { @@ -71,6 +91,7 @@ public class RabbitMqIntegrationListenerServiceTests _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); @@ -81,10 +102,17 @@ public class RabbitMqIntegrationListenerServiceTests Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), Arg.Any()); + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains("Non-retryable failure")), + Arg.Any(), + Arg.Any>()); + await _rabbitMqService.DidNotReceiveWithAnyArgs() - .RepublishToRetryQueueAsync(default, default); + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToRetryAsync(default, default, default); + .PublishToRetryAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -110,6 +138,7 @@ public class RabbitMqIntegrationListenerServiceTests _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); @@ -119,10 +148,17 @@ public class RabbitMqIntegrationListenerServiceTests Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), Arg.Any()); + _logger.Received().Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains("Max retry attempts reached")), + Arg.Any(), + Arg.Any>()); + await _rabbitMqService.DidNotReceiveWithAnyArgs() - .RepublishToRetryQueueAsync(default, default); + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToRetryAsync(default, default, default); + .PublishToRetryAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -149,6 +185,7 @@ public class RabbitMqIntegrationListenerServiceTests _handler.HandleAsync(Arg.Any()).Returns(result); var expected = IntegrationMessage.FromJson(message.ToJson()); + Assert.NotNull(expected); await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); @@ -161,9 +198,9 @@ public class RabbitMqIntegrationListenerServiceTests Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .RepublishToRetryQueueAsync(default, default); + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToDeadLetterAsync(default, default, default); + .PublishToDeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -191,11 +228,11 @@ public class RabbitMqIntegrationListenerServiceTests await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson())); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .RepublishToRetryQueueAsync(default, default); + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToRetryAsync(default, default, default); + .PublishToRetryAsync(Arg.Any(), Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToDeadLetterAsync(default, default, default); + .PublishToDeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -221,10 +258,10 @@ public class RabbitMqIntegrationListenerServiceTests await _rabbitMqService.Received(1) .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); - await _handler.DidNotReceiveWithAnyArgs().HandleAsync(default); + await _handler.DidNotReceiveWithAnyArgs().HandleAsync(Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToRetryAsync(default, default, default); + .PublishToRetryAsync(Arg.Any(), Arg.Any(), Arg.Any()); await _rabbitMqService.DidNotReceiveWithAnyArgs() - .PublishToDeadLetterAsync(default, default, default); + .PublishToDeadLetterAsync(Arg.Any(), Arg.Any(), Arg.Any()); } } From 5485c124454252d818c11bb1c0efd92675a2a4f9 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 1 Aug 2025 09:43:37 -0500 Subject: [PATCH 118/326] PM-24367 add personal_id to onyx api call (#6154) --- src/Billing/BillingSettings.cs | 1 + src/Billing/Controllers/FreshdeskController.cs | 2 +- src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs | 3 ++- src/Billing/appsettings.Development.json | 5 +++++ src/Billing/appsettings.Production.json | 5 ++++- src/Billing/appsettings.json | 3 ++- 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 9189fe6cd0..5609879eeb 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -40,5 +40,6 @@ public class BillingSettings { public virtual string ApiKey { get; set; } public virtual string BaseUrl { get; set; } + public virtual int PersonaId { get; set; } } } diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 3b7415121e..3f26e28786 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -153,7 +153,7 @@ public class FreshdeskController : Controller } // create the onyx `answer-with-citation` request - var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText); + 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)) { diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index c21ea9fc19..65cb2a9fca 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -20,11 +20,12 @@ public class OnyxAnswerWithCitationRequestModel [JsonPropertyName("retrieval_options")] public RetrievalOptions RetrievalOptions { get; set; } - public OnyxAnswerWithCitationRequestModel(string message) + public OnyxAnswerWithCitationRequestModel(string message, int personaId = 1) { message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' '); Messages = new List() { new Message() { MessageText = message } }; RetrievalOptions = new RetrievalOptions(); + PersonaId = personaId; } } diff --git a/src/Billing/appsettings.Development.json b/src/Billing/appsettings.Development.json index 32253a93c1..7c4889c22f 100644 --- a/src/Billing/appsettings.Development.json +++ b/src/Billing/appsettings.Development.json @@ -31,5 +31,10 @@ "storage": { "connectionString": "UseDevelopmentStorage=true" } + }, + "billingSettings": { + "onyx": { + "personaId": 68 + } } } diff --git a/src/Billing/appsettings.Production.json b/src/Billing/appsettings.Production.json index 819986181f..4be5d51a52 100644 --- a/src/Billing/appsettings.Production.json +++ b/src/Billing/appsettings.Production.json @@ -26,7 +26,10 @@ "payPal": { "production": true, "businessId": "4ZDA7DLUUJGMN" - } + }, + "onyx": { + "personaId": 7 + } }, "Logging": { "IncludeScopes": false, diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index 2a2864b246..aae25dde0b 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -76,7 +76,8 @@ }, "onyx": { "apiKey": "SECRET", - "baseUrl": "https://cloud.onyx.app/api" + "baseUrl": "https://cloud.onyx.app/api", + "personaId": 7 } } } From 2908ddb759d5c72f3cfa4af36ddeba8bca8844b6 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Fri, 1 Aug 2025 14:40:43 -0400 Subject: [PATCH 119/326] [PM-22692] Fix Secrets Manager Seat and ServiceAccount Limit Bug (#6138) * test: add new test harnesses * feat: update autoscale limit logic for SM Subscription Command * fix: remove redundant helper methods * fix: add periods to second sentence of templates --- ...zationSmServiceAccountsMaxReached.html.hbs | 2 +- ...zationSmServiceAccountsMaxReached.text.hbs | 2 +- .../SecretsManagerSubscriptionUpdate.cs | 4 - ...UpdateSecretsManagerSubscriptionCommand.cs | 77 +++++++--- ...eSecretsManagerSubscriptionCommandTests.cs | 141 ++++++++++++++++-- 5 files changed, 193 insertions(+), 33 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.html.hbs index 1f4300c23e..efeab22b9b 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationSmServiceAccountsMaxReached.html.hbs @@ -6,7 +6,7 @@ - Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created + Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created. BasicTextLayout}} -Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created +Your organization has reached the Secrets Manager machine accounts limit of {{MaxServiceAccountsCount}}. New machine accounts cannot be created. For more information, please refer to the following help article: https://bitwarden.com/help/managing-users {{/BasicTextLayout}} diff --git a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs index d85925db34..c813fd5b45 100644 --- a/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs +++ b/src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs @@ -50,11 +50,7 @@ public class SecretsManagerSubscriptionUpdate public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats; public bool MaxAutoscaleSmServiceAccountsChanged => MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts; - public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats; - public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue && - MaxAutoscaleSmServiceAccounts.HasValue && - SmServiceAccounts == MaxAutoscaleSmServiceAccounts; public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling) { diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs index 88b995be64..f7d6f0e5a2 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpdateSecretsManagerSubscriptionCommand.cs @@ -55,15 +55,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs await FinalizeSubscriptionAdjustmentAsync(update); - if (update.SmSeatAutoscaleLimitReached) - { - await SendSeatLimitEmailAsync(update.Organization); - } - - if (update.SmServiceAccountAutoscaleLimitReached) - { - await SendServiceAccountLimitEmailAsync(update.Organization); - } + await ValidateAutoScaleLimitsAsync(update); } private async Task FinalizeSubscriptionAdjustmentAsync(SecretsManagerSubscriptionUpdate update) @@ -100,7 +92,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs OrganizationUserType.Owner)) .Select(u => u.Email).Distinct(); - await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats.Value, ownerEmails); + await _mailService.SendSecretsManagerMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmSeats!.Value, ownerEmails); } catch (Exception e) @@ -117,7 +109,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs OrganizationUserType.Owner)) .Select(u => u.Email).Distinct(); - await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts.Value, ownerEmails); + await _mailService.SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(organization, organization.MaxAutoscaleSmServiceAccounts!.Value, ownerEmails); } catch (Exception e) @@ -197,7 +189,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs throw new BadRequestException("Organization has no Secrets Manager seat limit, no need to adjust seats"); } - if (update.Autoscaling && update.SmSeats.Value < organization.SmSeats.Value) + if (update.Autoscaling && update.SmSeats!.Value < organization.SmSeats.Value) { throw new BadRequestException("Cannot use autoscaling to subtract seats."); } @@ -211,7 +203,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs } // Check autoscale maximum seats - if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats.Value > update.MaxAutoscaleSmSeats.Value) + if (update.MaxAutoscaleSmSeats.HasValue && update.SmSeats!.Value > update.MaxAutoscaleSmSeats.Value) { var message = update.Autoscaling ? "Secrets Manager seat limit has been reached." @@ -220,7 +212,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs } // Check minimum seats included with plan - if (plan.SecretsManager.BaseSeats > update.SmSeats.Value) + if (plan.SecretsManager.BaseSeats > update.SmSeats!.Value) { throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseSeats} Secrets Manager seats."); } @@ -260,7 +252,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs throw new BadRequestException("Organization has no machine accounts limit, no need to adjust machine accounts"); } - if (update.Autoscaling && update.SmServiceAccounts.Value < organization.SmServiceAccounts.Value) + if (update.Autoscaling && update.SmServiceAccounts!.Value < organization.SmServiceAccounts.Value) { throw new BadRequestException("Cannot use autoscaling to subtract machine accounts."); } @@ -276,7 +268,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs // Check autoscale maximum service accounts if (update.MaxAutoscaleSmServiceAccounts.HasValue && - update.SmServiceAccounts.Value > update.MaxAutoscaleSmServiceAccounts.Value) + update.SmServiceAccounts!.Value > update.MaxAutoscaleSmServiceAccounts.Value) { var message = update.Autoscaling ? "Secrets Manager machine account limit has been reached." @@ -285,7 +277,7 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs } // Check minimum service accounts included with plan - if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts.Value) + if (plan.SecretsManager.BaseServiceAccount > update.SmServiceAccounts!.Value) { throw new BadRequestException($"Plan has a minimum of {plan.SecretsManager.BaseServiceAccount} machine accounts."); } @@ -379,4 +371,55 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs await _eventService.LogOrganizationEventAsync(org, orgEvent.Value); } } + + private async Task ValidateAutoScaleLimitsAsync(SecretsManagerSubscriptionUpdate update) + { + var (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached) = await AreAutoscaleLimitsReachedAsync(update); + + if (smSeatAutoScaleLimitReached) + { + await SendSeatLimitEmailAsync(update.Organization); + } + + if (smServiceAccountsLimitReached) + { + await SendServiceAccountLimitEmailAsync(update.Organization); + } + } + + private async Task<(bool, bool)> AreAutoscaleLimitsReachedAsync(SecretsManagerSubscriptionUpdate update) + { + var smSeatAutoScaleLimitReached = false; + var smServiceAccountsLimitReached = false; + + var (occupiedSmSeats, occupiedSmServiceAccounts) = await GetOccupiedSmSeatsAndServiceAccountsAsync(update.Organization.Id); + + if (occupiedSmSeats > 0 + && update.MaxAutoscaleSmSeats is not null + && occupiedSmSeats == update.MaxAutoscaleSmSeats!.Value) + { + smSeatAutoScaleLimitReached = true; + } + + if (occupiedSmServiceAccounts > 0 + && update.MaxAutoscaleSmServiceAccounts is not null + && occupiedSmServiceAccounts == update.MaxAutoscaleSmServiceAccounts!.Value) + { + smServiceAccountsLimitReached = true; + } + + return (smSeatAutoScaleLimitReached, smServiceAccountsLimitReached); + } + + /// + /// Requests the number of Secret Manager seats and service accounts are currently used by the organization + /// + /// The id of the organization + /// A tuple containing the occupied seats and the occupied service account counts + private async Task<(int, int)> GetOccupiedSmSeatsAndServiceAccountsAsync(Guid organizationId) + { + var occupiedSmSeatsTask = _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organizationId); + var occupiedServiceAccountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(organizationId); + return (await occupiedSmSeatsTask, await occupiedServiceAccountsTask); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs index 50f51da7d0..8b00741215 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpdateSecretsManagerSubscriptionCommandTests.cs @@ -1,7 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Repositories; @@ -99,8 +101,13 @@ public class UpdateSecretsManagerSubscriptionCommandTests org.MaxAutoscaleSmServiceAccounts == updateMaxAutoscaleSmServiceAccounts), sutProvider); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxSeatLimitReachedEmailAsync(default, default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(default, default, default); + await sutProvider + .GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSecretsManagerMaxSeatLimitReachedEmailAsync(default, default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(default, default, default); } [Theory] @@ -266,11 +273,13 @@ public class UpdateSecretsManagerSubscriptionCommandTests [Theory] [BitAutoData] - public async Task UpdateSubscriptionAsync_UpdateSeatsToAutoscaleLimit_EmailsOwners( + public async Task UpdateSubscriptionAsync_UpdateSeatCount_AndExistingSeatsDoNotReachAutoscaleLimit_NoEmailSent( Organization organization, 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; @@ -281,11 +290,66 @@ public class UpdateSecretsManagerSubscriptionCommandTests SmSeats = seatCount, MaxAutoscaleSmSeats = seatCount }; + sutProvider.GetDependency() + .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) + .Returns(existingSeatCount); + // Act await sutProvider.Sut.UpdateSubscriptionAsync(update); - await sutProvider.GetDependency().Received(1).SendSecretsManagerMaxSeatLimitReachedEmailAsync( - organization, organization.MaxAutoscaleSmSeats.Value, Arg.Any>()); + // Assert + + // Currently being called once each for different validation methods + await sutProvider.GetDependency() + .Received(2) + .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Any(), Arg.Any(), Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task UpdateSubscriptionAsync_ExistingSeatsReachAutoscaleLimit_EmailOwners( + Organization organization, + SutProvider sutProvider) + { + // Arrange + const int seatCount = 10; + const int existingSeatCount = 10; + var ownerDetailsList = new List { new() { Email = "owner@example.com" } }; + + // The amount of seats for users in an organization + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) + { + SmSeats = seatCount, + MaxAutoscaleSmSeats = seatCount + }; + + sutProvider.GetDependency() + .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id) + .Returns(existingSeatCount); + sutProvider.GetDependency() + .GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner) + .Returns(ownerDetailsList); + + // Act + await sutProvider.Sut.UpdateSubscriptionAsync(update); + + // Assert + + // Currently being called once each for different validation methods + await sutProvider.GetDependency() + .Received(2) + .GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); + + await sutProvider.GetDependency() + .Received(1) + .SendSecretsManagerMaxSeatLimitReachedEmailAsync(Arg.Is(organization), + Arg.Is(seatCount), + Arg.Is>(emails => emails.Contains(ownerDetailsList[0].Email))); } [Theory] @@ -413,21 +477,78 @@ public class UpdateSecretsManagerSubscriptionCommandTests [Theory] [BitAutoData] - public async Task UpdateSubscriptionAsync_UpdateServiceAccountsToAutoscaleLimit_EmailsOwners( + public async Task UpdateSubscriptionAsync_UpdateServiceAccounts_AndExistingServiceAccountsCountDoesNotReachAutoscaleLimit_NoEmailSent( Organization organization, SutProvider sutProvider) { + // Arrange + var smServiceAccounts = 300; + var existingServiceAccountCount = 299; + var plan = StaticStore.GetPlan(organization.PlanType); var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) { - SmServiceAccounts = 300, - MaxAutoscaleSmServiceAccounts = 300 + SmServiceAccounts = smServiceAccounts, + MaxAutoscaleSmServiceAccounts = smServiceAccounts }; + sutProvider.GetDependency() + .GetServiceAccountCountByOrganizationIdAsync(organization.Id) + .Returns(existingServiceAccountCount); + // Act await sutProvider.Sut.UpdateSubscriptionAsync(update); - await sutProvider.GetDependency().Received(1).SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync( - organization, organization.MaxAutoscaleSmServiceAccounts.Value, Arg.Any>()); + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetServiceAccountCountByOrganizationIdAsync(organization.Id); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync( + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task UpdateSubscriptionAsync_ExistingServiceAccountsReachAutoscaleLimit_EmailOwners( + Organization organization, + SutProvider sutProvider) + { + var smServiceAccounts = 300; + var plan = StaticStore.GetPlan(organization.PlanType); + var update = new SecretsManagerSubscriptionUpdate(organization, plan, false) + { + SmServiceAccounts = smServiceAccounts, + MaxAutoscaleSmServiceAccounts = smServiceAccounts + }; + var ownerDetailsList = new List { new() { Email = "owner@example.com" } }; + + + sutProvider.GetDependency() + .GetServiceAccountCountByOrganizationIdAsync(organization.Id) + .Returns(smServiceAccounts); + sutProvider.GetDependency() + .GetManyByMinimumRoleAsync(organization.Id, OrganizationUserType.Owner) + .Returns(ownerDetailsList); + + + // Act + await sutProvider.Sut.UpdateSubscriptionAsync(update); + + // Assert + + await sutProvider.GetDependency() + .Received(1) + .GetServiceAccountCountByOrganizationIdAsync(organization.Id); + + await sutProvider.GetDependency() + .Received(1) + .SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Arg.Is(organization), + Arg.Is(smServiceAccounts), + Arg.Is>(emails => emails.Contains(ownerDetailsList[0].Email))); } [Theory] From 1c2bccdeff6fbee3e36e260933ae35274b0deaff Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 4 Aug 2025 17:26:39 +0000 Subject: [PATCH 120/326] Bumped version to 2025.8.0 --- Directory.Build.props | 45 ++++++++++++------------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 44df15a593..d8af8dc990 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,61 +3,40 @@ net8.0 - 2025.7.2 + 2025.8.0 Bit.$(MSBuildProjectName) enable false - + true annotations enable true - + - + 17.8.0 - + 2.6.6 - + 2.5.6 - + 6.0.0 - + 5.1.0 - + 4.18.1 - + 4.18.1 - + - + From 9081c205b1bc5e9d57af747e2227dca1a50d5002 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:20:54 -0400 Subject: [PATCH 121/326] [BRE-1058] fix alpine race condition (#6156) * alpine race condition during shutdown fix * change catch to only be for relevant task cancelled, added a debug log * test commit for build and test * remove testing comment --- .../ApplicationCacheHostedService.cs | 50 +++++++++++++++---- .../AzureQueueHostedService.cs | 15 +++++- src/Notifications/AzureQueueHostedService.cs | 5 ++ 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/Core/HostedServices/ApplicationCacheHostedService.cs b/src/Core/HostedServices/ApplicationCacheHostedService.cs index a699a26fcc..ca2744bd10 100644 --- a/src/Core/HostedServices/ApplicationCacheHostedService.cs +++ b/src/Core/HostedServices/ApplicationCacheHostedService.cs @@ -67,19 +67,25 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable public virtual async Task StopAsync(CancellationToken cancellationToken) { + // Step 1: Signal ExecuteAsync to stop gracefully + _cts?.Cancel(); + + // Step 2: Wait for ExecuteAsync to finish cleanly + if (_executingTask != null) + { + await _executingTask; + } + + // Step 3: Now safely dispose resources (ExecuteAsync is done) await _subscriptionReceiver.CloseAsync(cancellationToken); await _serviceBusClient.DisposeAsync(); - _cts?.Cancel(); + + // Step 4: Clean up subscription try { await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken); } catch { } - - if (_executingTask != null) - { - await _executingTask; - } } public virtual void Dispose() @@ -87,15 +93,39 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable private async Task ExecuteAsync(CancellationToken cancellationToken) { - await foreach (var message in _subscriptionReceiver.ReceiveMessagesAsync(cancellationToken)) + while (!cancellationToken.IsCancellationRequested) { try { - await ProcessMessageAsync(message, cancellationToken); + var messages = await _subscriptionReceiver.ReceiveMessagesAsync( + maxMessages: 1, + maxWaitTime: TimeSpan.FromSeconds(30), + cancellationToken); + + if (messages?.Any() == true) + { + foreach (var message in messages) + { + try + { + await ProcessMessageAsync(message, cancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "Error processing messages in ApplicationCacheHostedService"); + } + } + } } - catch (Exception e) + catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) { - _logger.LogError(e, "Error processing messages in ApplicationCacheHostedService"); + _logger.LogDebug("ServiceBus receiver disposed during Alpine container shutdown"); + break; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("ServiceBus operation cancelled during Alpine container shutdown"); + break; } } } diff --git a/src/EventsProcessor/AzureQueueHostedService.cs b/src/EventsProcessor/AzureQueueHostedService.cs index 1f72fbb9c8..c6f5afbfdd 100644 --- a/src/EventsProcessor/AzureQueueHostedService.cs +++ b/src/EventsProcessor/AzureQueueHostedService.cs @@ -86,11 +86,24 @@ public class AzureQueueHostedService : IHostedService, IDisposable await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); } } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("Task.Delay cancelled during Alpine container shutdown"); + break; + } catch (Exception ex) { _logger.LogError(ex, "Error occurred processing message block."); - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + try + { + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("Task.Delay cancelled during Alpine container shutdown"); + break; + } } } diff --git a/src/Notifications/AzureQueueHostedService.cs b/src/Notifications/AzureQueueHostedService.cs index c67e6b6986..94aa14eaf6 100644 --- a/src/Notifications/AzureQueueHostedService.cs +++ b/src/Notifications/AzureQueueHostedService.cs @@ -87,6 +87,11 @@ public class AzureQueueHostedService : IHostedService, IDisposable await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); } } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug("Task.Delay cancelled during Alpine container shutdown"); + break; + } catch (Exception e) { _logger.LogError(e, "Error processing messages."); From 11cc50af6e8ac2f3a1075b4f1b224484db44b295 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Tue, 5 Aug 2025 09:50:36 -0400 Subject: [PATCH 122/326] Update scan workflow to use centralized reusable component (#6127) --- .github/workflows/scan.yml | 113 ++++++------------------------------- 1 file changed, 16 insertions(+), 97 deletions(-) diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index eb72867fc4..f1d9370c29 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -16,6 +16,8 @@ on: branches: - "main" +permissions: {} + jobs: check-run: name: Check PR run @@ -24,113 +26,30 @@ jobs: contents: read sast: - name: SAST scan - runs-on: ubuntu-22.04 + name: Checkmarx + uses: bitwarden/gh-actions/.github/workflows/_checkmarx.yml@main needs: check-run + secrets: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} permissions: contents: read pull-requests: write security-events: write id-token: write - steps: - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - ref: ${{ github.event.pull_request.head.sha }} - - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Get Azure Key Vault secrets - id: get-kv-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-org-bitwarden - secrets: "CHECKMARX-TENANT,CHECKMARX-CLIENT-ID,CHECKMARX-SECRET" - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41 - env: - INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" - with: - project_name: ${{ github.repository }} - cx_tenant: ${{ steps.get-kv-secrets.outputs.CHECKMARX-TENANT }} - base_uri: https://ast.checkmarx.net/ - cx_client_id: ${{ steps.get-kv-secrets.outputs.CHECKMARX-CLIENT-ID }} - cx_client_secret: ${{ steps.get-kv-secrets.outputs.CHECKMARX-SECRET }} - additional_params: | - --report-format sarif \ - --filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \ - --output-path . ${{ env.INCREMENTAL }} - - - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 - with: - sarif_file: cx_result.sarif - sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} - ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - quality: - name: Quality scan - runs-on: ubuntu-22.04 + name: Sonar + uses: bitwarden/gh-actions/.github/workflows/_sonar.yml@main needs: check-run + secrets: + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} permissions: contents: read pull-requests: write id-token: write - - steps: - - name: Set up JDK 17 - uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 - with: - java-version: 17 - distribution: "zulu" - - - name: Check out repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 0 - ref: ${{ github.event.pull_request.head.sha }} - - - name: Set up .NET - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 - - - name: Install SonarCloud scanner - run: dotnet tool install dotnet-sonarscanner -g - - - name: Log in to Azure - uses: bitwarden/gh-actions/azure-login@main - with: - subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - tenant_id: ${{ secrets.AZURE_TENANT_ID }} - client_id: ${{ secrets.AZURE_CLIENT_ID }} - - - name: Get Azure Key Vault secrets - id: get-kv-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: gh-org-bitwarden - secrets: "SONAR-TOKEN" - - - name: Log out from Azure - uses: bitwarden/gh-actions/azure-logout@main - - - name: Scan with SonarCloud - env: - SONAR_TOKEN: ${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }} - run: | - dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \ - /d:sonar.test.inclusions=test/,bitwarden_license/test/ \ - /d:sonar.exclusions=test/,bitwarden_license/test/ \ - /o:"${{ github.repository_owner }}" /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" \ - /d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }} - dotnet build - dotnet-sonarscanner end /d:sonar.token="${{ steps.get-kv-secrets.outputs.SONAR-TOKEN }}" + with: + sonar-config: "dotnet" From 7454430aa16ad1719a11898356cdf7570b9c0e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 5 Aug 2025 15:34:13 +0100 Subject: [PATCH 123/326] =?UTF-8?q?[PM-22241]=C2=A0Add=20DefaultUserCollec?= =?UTF-8?q?tionName=20support=20to=20bulk=20organization=20user=20confirma?= =?UTF-8?q?tion=20(#6153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement GetByOrganizationAsync method in PolicyRequirementQuery and add corresponding unit tests * Refactor ConfirmOrganizationUserCommand for clarity and add bulk support * Update ConfirmOrganizationUserCommandTests to use GetByOrganizationAsync for policy requirement queries * Add DefaultUserCollectionName property to OrganizationUserBulkConfirmRequestModel with encryption attributes * Update ConfirmUsersAsync method to include DefaultUserCollectionName parameter in OrganizationUsersController * Add EnableOrganizationDataOwnershipPolicyAsync method to OrganizationTestHelpers * Add integration tests for confirming organization users in OrganizationUserControllerTests - Implemented Confirm_WithValidUser test to verify successful confirmation of a single user. - Added BulkConfirm_WithValidUsers test to ensure multiple users can be confirmed successfully. * Refactor organization user confirmation integration tests to also test when the organization data ownership policy is disabled * Refactor ConfirmOrganizationUserCommand to consolidate confirmation side effects handling - Replaced single and bulk confirmation side effect methods with a unified HandleConfirmationSideEffectsAsync method. - Updated related logic to handle confirmed organization users more efficiently. - Adjusted unit tests to reflect changes in the collection creation process for confirmed users. * Refactor OrganizationUserControllerTests to simplify feature flag handling and consolidate test logic - Removed redundant feature flag checks in Confirm and BulkConfirm tests. - Updated tests to directly enable the Organization Data Ownership policy without conditional checks. - Ensured verification of DefaultUserCollection for confirmed users remains intact. * Refactor OrganizationUserControllerTests to enhance clarity and reduce redundancy - Simplified user creation and confirmation logic in tests by introducing helper methods. - Consolidated verification of confirmed users and their associated collections. - Removed unnecessary comments and streamlined test flow for better readability. --- .../OrganizationUsersController.cs | 2 +- .../OrganizationUserRequestModels.cs | 4 + .../ConfirmOrganizationUserCommand.cs | 74 ++++++----- .../IConfirmOrganizationUserCommand.cs | 3 +- .../Policies/IPolicyRequirementQuery.cs | 10 ++ .../Implementations/PolicyRequirementQuery.cs | 22 ++++ .../OrganizationUserControllerTests.cs | 119 ++++++++++++++++++ .../Helpers/OrganizationTestHelpers.cs | 20 +++ .../ConfirmOrganizationUserCommandTests.cs | 19 ++- .../Policies/PolicyRequirementQueryTests.cs | 69 ++++++++++ 10 files changed, 294 insertions(+), 48 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 3365e754ca..bf49f144ce 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -340,7 +340,7 @@ public class OrganizationUsersController : Controller [FromBody] OrganizationUserBulkConfirmRequestModel model) { var userId = _userService.GetProperUserId(User); - var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgId, model.ToDictionary(), userId.Value); + var results = await _confirmOrganizationUserCommand.ConfirmUsersAsync(orgId, model.ToDictionary(), userId.Value, model.DefaultUserCollectionName); return new ListResponseModel(results.Select(r => new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2))); diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs index b4d3326013..4e0accb9e8 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -82,6 +82,10 @@ public class OrganizationUserBulkConfirmRequestModel [Required] public IEnumerable Keys { get; set; } + [EncryptedString] + [EncryptedStringLength(1000)] + public string DefaultUserCollectionName { get; set; } + public Dictionary ToDictionary() { return Keys.ToDictionary(e => e.Id, e => e.Key); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 6ec69312ad..0baa9c9e3a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -11,7 +11,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -67,7 +66,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, string defaultUserCollectionName = null) { - var result = await ConfirmUsersAsync( + var result = await SaveChangesToDatabaseAsync( organizationId, new Dictionary() { { organizationUserId, key } }, confirmingUserId); @@ -83,12 +82,30 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand throw new BadRequestException(error); } - await HandleConfirmationSideEffectsAsync(organizationId, orgUser, defaultUserCollectionName); + await HandleConfirmationSideEffectsAsync(organizationId, confirmedOrganizationUsers: [orgUser], defaultUserCollectionName); return orgUser; } public async Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, + Guid confirmingUserId, string defaultUserCollectionName = null) + { + var result = await SaveChangesToDatabaseAsync(organizationId, keys, confirmingUserId); + + var confirmedOrganizationUsers = result + .Where(r => string.IsNullOrEmpty(r.Item2)) + .Select(r => r.Item1) + .ToList(); + + if (confirmedOrganizationUsers.Count > 0) + { + await HandleConfirmationSideEffectsAsync(organizationId, confirmedOrganizationUsers, defaultUserCollectionName); + } + + return result; + } + + private async Task>> SaveChangesToDatabaseAsync(Guid organizationId, Dictionary keys, Guid confirmingUserId) { var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys); @@ -227,17 +244,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand .Select(d => d.Id.ToString()); } - private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, OrganizationUser organizationUser, string defaultUserCollectionName) - { - // Create DefaultUserCollection type collection for the user if the OrganizationDataOwnership policy is enabled for the organization - var requiresDefaultCollection = await OrganizationRequiresDefaultCollectionAsync(organizationId, organizationUser.UserId.Value, defaultUserCollectionName); - if (requiresDefaultCollection) - { - await CreateDefaultCollectionAsync(organizationId, organizationUser.Id, defaultUserCollectionName); - } - } - - private async Task OrganizationRequiresDefaultCollectionAsync(Guid organizationId, Guid userId, string defaultUserCollectionName) + private async Task OrganizationRequiresDefaultCollectionAsync(Guid organizationId, string defaultUserCollectionName) { if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) { @@ -250,30 +257,29 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand return false; } - var organizationDataOwnershipRequirement = await _policyRequirementQuery.GetAsync(userId); - return organizationDataOwnershipRequirement.RequiresDefaultCollection(organizationId); + var organizationPolicyRequirement = await _policyRequirementQuery.GetByOrganizationAsync(organizationId); + + // Check if the organization requires default collections + return organizationPolicyRequirement.RequiresDefaultCollection(organizationId); } - private async Task CreateDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName) + /// + /// Handles the side effects of confirming an organization user. + /// Creates a default collection for the user if the organization + /// has the OrganizationDataOwnership policy enabled. + /// + /// The organization ID. + /// The confirmed organization users. + /// The encrypted default user collection name. + private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName) { - var collection = new Collection + var requiresDefaultCollections = await OrganizationRequiresDefaultCollectionAsync(organizationId, defaultUserCollectionName); + if (!requiresDefaultCollections) { - OrganizationId = organizationId, - Name = defaultCollectionName, - Type = CollectionType.DefaultUserCollection - }; + return; + } - var userAccess = new List - { - new CollectionAccessSelection - { - Id = organizationUserId, - ReadOnly = false, - HidePasswords = false, - Manage = true - } - }; - - await _collectionRepository.CreateAsync(collection, groups: null, users: userAccess); + var organizationUserIds = confirmedOrganizationUsers.Select(u => u.Id).ToList(); + await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultUserCollectionName); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index cf5999f892..aca4853b66 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -29,7 +29,8 @@ public interface IConfirmOrganizationUserCommand /// The ID of the organization. /// A dictionary mapping organization user IDs to their encrypted organization keys. /// The ID of the user performing the confirmation. + /// Optional encrypted collection name for creating default collections. /// A list of tuples containing the organization user and an error message (if any). Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, - Guid confirmingUserId); + Guid confirmingUserId, string defaultUserCollectionName = null); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs index 5736078f22..226347fe29 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -15,4 +15,14 @@ public interface IPolicyRequirementQuery /// The user that you need to enforce the policy against. /// The IPolicyRequirement that corresponds to the policy you want to enforce. Task GetAsync(Guid userId) where T : IPolicyRequirement; + + /// + /// Get a policy requirement for a specific organization. + /// This returns the policy requirement that represents the policy state for the entire organization. + /// It will always return a value even if there are no policies that should be enforced. + /// This should be used for organization-level policy checks. + /// + /// The organization to check policies for. + /// The IPolicyRequirement that corresponds to the policy you want to enforce. + Task GetByOrganizationAsync(Guid organizationId) where T : IPolicyRequirement; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index de4796d4b5..ba4495224c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -1,5 +1,6 @@ #nullable enable +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; @@ -27,6 +28,27 @@ public class PolicyRequirementQuery( return requirement; } + public async Task GetByOrganizationAsync(Guid organizationId) where T : IPolicyRequirement + { + var factory = factories.OfType>().SingleOrDefault(); + if (factory is null) + { + throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); + } + + var organizationPolicyDetails = await GetOrganizationPolicyDetails(organizationId, factory.PolicyType); + var filteredPolicies = organizationPolicyDetails + .Cast() + .Where(policyDetails => policyDetails.PolicyType == factory.PolicyType) + .Where(factory.Enforce) + .ToList(); + var requirement = factory.Create(filteredPolicies); + return requirement; + } + private Task> GetPolicyDetails(Guid userId) => policyRepository.GetPolicyDetailsByUserId(userId); + + private async Task> GetOrganizationPolicyDetails(Guid organizationId, PolicyType policyType) + => await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType); } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index ca13585017..c60c2049a1 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -2,19 +2,34 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; +using Bit.Core; using Bit.Core.AdminConsole.Entities; 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.Services; +using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; public class OrganizationUserControllerTests : IClassFixture, IAsyncLifetime { + private static readonly string _mockEncryptedString = + "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; + + public OrganizationUserControllerTests(ApiApplicationFactory apiFactory) { _factory = apiFactory; + _factory.SubstituteService(featureService => + { + featureService + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + }); _client = _factory.CreateClient(); _loginHelper = new LoginHelper(_factory, _client); } @@ -93,9 +108,113 @@ public class OrganizationUserControllerTests : IClassFixture new OrganizationUserBulkConfirmRequestModelEntry + { + Id = organizationUser.Id, + Key = string.Format(testKeyFormat, index) + }), + DefaultUserCollectionName = _mockEncryptedString + }; + + var bulkConfirmResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/confirm", bulkConfirmModel); + + Assert.Equal(HttpStatusCode.OK, bulkConfirmResponse.StatusCode); + + await VerifyMultipleUsersConfirmedAsync(acceptedUsers.Select((organizationUser, index) => + (organizationUser, string.Format(testKeyFormat, index))).ToList()); + await VerifyMultipleUsersHaveDefaultCollectionsAsync(acceptedUsers); + } + public Task DisposeAsync() { _client.Dispose(); return Task.CompletedTask; } + + private async Task> CreateAcceptedUsersAsync(IEnumerable emails) + { + var acceptedUsers = new List(); + + foreach (var email in emails) + { + await _factory.LoginWithNewAccount(email); + + var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, + OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted); + + acceptedUsers.Add(acceptedOrgUser); + } + + return acceptedUsers; + } + + private async Task VerifyDefaultCollectionCreatedAsync(OrganizationUser orgUser) + { + var collectionRepository = _factory.GetService(); + var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value); + Assert.Single(collections); + Assert.Equal(_mockEncryptedString, collections.First().Name); + } + + private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey) + { + await VerifyMultipleUsersConfirmedAsync(new List<(OrganizationUser orgUser, string key)> { (orgUser, expectedKey) }); + } + + private async Task VerifyMultipleUsersConfirmedAsync(List<(OrganizationUser orgUser, string key)> acceptedOrganizationUsers) + { + var orgUserRepository = _factory.GetService(); + for (int i = 0; i < acceptedOrganizationUsers.Count; i++) + { + var confirmedUser = await orgUserRepository.GetByIdAsync(acceptedOrganizationUsers[i].orgUser.Id); + Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status); + Assert.Equal(acceptedOrganizationUsers[i].key, confirmedUser.Key); + } + } + + private async Task VerifyMultipleUsersHaveDefaultCollectionsAsync(List acceptedOrganizationUsers) + { + var collectionRepository = _factory.GetService(); + foreach (var acceptedOrganizationUser in acceptedOrganizationUsers) + { + var collections = await collectionRepository.GetManyByUserIdAsync(acceptedOrganizationUser.UserId!.Value); + Assert.Single(collections); + Assert.Equal(_mockEncryptedString, collections.First().Name); + } + } } diff --git a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs index ae4e27267d..fb5c9bbc56 100644 --- a/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs +++ b/test/Api.IntegrationTest/Helpers/OrganizationTestHelpers.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using Bit.Api.IntegrationTest.Factories; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; @@ -148,4 +149,23 @@ public static class OrganizationTestHelpers await groupRepository.CreateAsync(group, new List()); return group; } + + /// + /// Enables the Organization Data Ownership policy for the specified organization. + /// + public static async Task EnableOrganizationDataOwnershipPolicyAsync( + WebApplicationFactoryBase factory, + Guid organizationId) where T : class + { + var policyRepository = factory.GetService(); + + var policy = new Policy + { + OrganizationId = organizationId, + Type = PolicyType.OrganizationDataOwnership, + Enabled = true + }; + + await policyRepository.CreateAsync(policy); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index a6709cd10b..b0815d9f35 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -473,7 +473,7 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) + .GetByOrganizationAsync(organization.Id) .Returns(new OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState.Enabled, [organization.Id])); @@ -482,15 +482,10 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency() .Received(1) - .CreateAsync( - Arg.Is(c => c.Name == collectionName && - c.OrganizationId == organization.Id && - c.Type == CollectionType.DefaultUserCollection), - Arg.Is>(groups => groups == null), - Arg.Is>(u => - u.Count() == 1 && - u.First().Id == orgUser.Id && - u.First().Manage == true)); + .CreateDefaultCollectionsAsync( + organization.Id, + Arg.Is>(ids => ids.Contains(orgUser.Id)), + collectionName); } [Theory, BitAutoData] @@ -510,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) + .GetByOrganizationAsync(org.Id) .Returns(new OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState.Enabled, [org.Id])); @@ -538,7 +533,7 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetAsync(user.Id) + .GetByOrganizationAsync(org.Id) .Returns(new OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState.Enabled, [Guid.NewGuid()])); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index 56b6740678..da8f7319d5 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -79,4 +79,73 @@ public class PolicyRequirementQueryTests Assert.Empty(requirement.Policies); } + + [Theory, BitAutoData] + public async Task GetByOrganizationAsync_IgnoresOtherPolicyTypes(Guid organizationId) + { + var policyRepository = Substitute.For(); + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = Guid.NewGuid() }; + // Force the repository to return both policies even though that is not the expected result + policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg) + .Returns([thisPolicy, otherPolicy]); + + var factory = new TestPolicyRequirementFactory(_ => true); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + var requirement = await sut.GetByOrganizationAsync(organizationId); + + await policyRepository.Received(1).GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg); + + Assert.Contains(thisPolicy, requirement.Policies.Cast()); + Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast()); + } + + [Theory, BitAutoData] + public async Task GetByOrganizationAsync_CallsEnforceCallback(Guid organizationId) + { + var policyRepository = Substitute.For(); + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; + policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([thisPolicy, otherPolicy]); + + var callback = Substitute.For>(); + callback(Arg.Any()).Returns(x => x.Arg() == thisPolicy); + + var factory = new TestPolicyRequirementFactory(callback); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + var requirement = await sut.GetByOrganizationAsync(organizationId); + + Assert.Contains(thisPolicy, requirement.Policies.Cast()); + Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast()); + callback.Received()(Arg.Is(p => p == thisPolicy)); + callback.Received()(Arg.Is(p => p == otherPolicy)); + } + + [Theory, BitAutoData] + public async Task GetByOrganizationAsync_ThrowsIfNoFactoryRegistered(Guid organizationId) + { + var policyRepository = Substitute.For(); + var sut = new PolicyRequirementQuery(policyRepository, []); + + var exception = await Assert.ThrowsAsync(() + => sut.GetByOrganizationAsync(organizationId)); + + Assert.Contains("No Requirement Factory found", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetByOrganizationAsync_HandlesNoPolicies(Guid organizationId) + { + var policyRepository = Substitute.For(); + policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([]); + + var factory = new TestPolicyRequirementFactory(x => x.IsProvider); + var sut = new PolicyRequirementQuery(policyRepository, [factory]); + + var requirement = await sut.GetByOrganizationAsync(organizationId); + + Assert.Empty(requirement.Policies); + } } From 14899eb883976dcd98e8c7766f2e3cade3666e97 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 5 Aug 2025 13:28:27 -0400 Subject: [PATCH 124/326] set version to 2025.7.3 (#6160) --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d8af8dc990..d8ace51f01 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.8.0 + 2025.7.3 Bit.$(MSBuildProjectName) enable From 25a54b16f750df98a45242000c547c2dfcf4e898 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 5 Aug 2025 21:36:04 +0000 Subject: [PATCH 125/326] Fix Dockerfiles that had BUILDPLATFORM specified for App Stages (#6162) --- src/Api/Dockerfile | 2 +- src/Billing/Dockerfile | 2 +- src/Identity/Dockerfile | 2 +- util/Setup/Dockerfile | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile index 5815d06769..beacee89ae 100644 --- a/src/Api/Dockerfile +++ b/src/Api/Dockerfile @@ -37,7 +37,7 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" diff --git a/src/Billing/Dockerfile b/src/Billing/Dockerfile index 8f1a217b0e..1e182dedff 100644 --- a/src/Billing/Dockerfile +++ b/src/Billing/Dockerfile @@ -37,7 +37,7 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" diff --git a/src/Identity/Dockerfile b/src/Identity/Dockerfile index 41f23f6957..e79439f275 100644 --- a/src/Identity/Dockerfile +++ b/src/Identity/Dockerfile @@ -37,7 +37,7 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" diff --git a/util/Setup/Dockerfile b/util/Setup/Dockerfile index 80d00315e4..2ab86c69ed 100644 --- a/util/Setup/Dockerfile +++ b/util/Setup/Dockerfile @@ -26,7 +26,6 @@ WORKDIR /source/util/Setup RUN . /tmp/rid.txt && dotnet restore -r $RID # Build project -WORKDIR /source/util/Setup RUN . /tmp/rid.txt && dotnet publish \ -c release \ --no-restore \ @@ -38,7 +37,7 @@ RUN . /tmp/rid.txt && dotnet publish \ ############################################### # App stage # ############################################### -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21 ARG TARGETPLATFORM LABEL com.bitwarden.product="bitwarden" com.bitwarden.project="setup" From 000d1f2f6ef3f6a679ed1b0d174cd18b8b8741dd Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:18:57 -0400 Subject: [PATCH 126/326] refactor(DeviceValidator): [Auth/PM-24362] Misc improvements (#6152) * PM-24362 - DeviceValidator - (1) refactor name of NewDeviceOtpRequest --> RequestHasNewDeviceVerificationOtp (2) Move auth request rejection check above normal NDV check and remove auth request check from NDV check * PM-24362 - Update DeviceValidatorTests + add new scenario --- .../RequestValidators/DeviceValidator.cs | 39 +++++++------- .../IdentityServer/DeviceValidatorTests.cs | 51 +++++++++++++++++-- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 80eb455519..d9a4fdb485 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -13,6 +13,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Identity.IdentityServer.Enums; +using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.Extensions.Caching.Distributed; @@ -55,8 +56,10 @@ public class DeviceValidator( return false; } - // if not a new device request then check if the device is known - if (!NewDeviceOtpRequest(request)) + // Check if the request has a NewDeviceOtp, if it does we can assume it is an unknown device + // that has already been prompted for new device verification so we don't + // have to hit the database to check if the device is known to avoid unnecessary database calls. + if (!RequestHasNewDeviceVerificationOtp(request)) { var knownDevice = await GetKnownDeviceAsync(context.User, requestDevice); // if the device is know then we return the device fetched from the database @@ -69,14 +72,23 @@ public class DeviceValidator( } } - // We have established that the device is unknown at this point; begin new device verification - // for standard password grant type requests - // Note: the auth request flow re-uses the resource owner password flow but new device verification - // is not required for auth requests + // The device is either unknown or the request has a NewDeviceOtp (implies unknown device) + var rawAuthRequestId = request.Raw["AuthRequest"]?.ToLowerInvariant(); var isAuthRequest = !string.IsNullOrEmpty(rawAuthRequestId); - if (request.GrantType == PasswordGrantType && - !isAuthRequest && + + // Device unknown, but if we are in an auth request flow, this is not valid + // as we only support auth request authN requests on known devices + // Note: we re-use the resource owner password flow for auth requests + if (request.GrantType == GrantType.ResourceOwnerPassword && isAuthRequest) + { + (context.ValidationErrorResult, context.CustomResponse) = + BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice); + return false; + } + + // Enforce new device verification for resource owner password flow (just normal password flow) + if (request.GrantType == GrantType.ResourceOwnerPassword && context is { TwoFactorRequired: false, SsoRequired: false } && _globalSettings.EnableNewDeviceVerification) { @@ -93,15 +105,6 @@ public class DeviceValidator( } } - // Device still unknown, but if we are in an auth request flow, this is not valid - // as we only support auth request authN requests on known devices - if (request.GrantType == PasswordGrantType && isAuthRequest) - { - (context.ValidationErrorResult, context.CustomResponse) = - BuildDeviceErrorResult(DeviceValidationResultType.AuthRequestFlowUnknownDevice); - return false; - } - // At this point we have established either new device verification is not required or the NewDeviceOtp is valid, // so we save the device to the database and proceed with authentication requestDevice.UserId = context.User.Id; @@ -247,7 +250,7 @@ public class DeviceValidator( /// /// /// - public static bool NewDeviceOtpRequest(ValidatedRequest request) + public static bool RequestHasNewDeviceVerificationOtp(ValidatedRequest request) { return !string.IsNullOrEmpty(request.Raw["NewDeviceOtp"]?.ToString()); } diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 551f34b90a..7cb16da4ec 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -9,6 +9,7 @@ using Bit.Core.Settings; using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.RequestValidators; using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; @@ -336,7 +337,7 @@ public class DeviceValidatorTests [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) { // Arrange - request.GrantType = "password"; + request.GrantType = GrantType.ResourceOwnerPassword; context.TwoFactorRequired = twoFactoRequired; context.SsoRequired = ssoRequired; if (context.User != null) @@ -359,6 +360,48 @@ public class DeviceValidatorTests var expectedErrorMessage = "auth request flow unsupported on unknown device"; var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; Assert.Equal(expectedErrorMessage, actualResponse.Message); + await _deviceService.Received(0).SaveAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(false, false)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + [BitAutoData(true, false)] + public async void ValidateRequestDeviceAsync_IsAuthRequest_NewDeviceOtp_Errors( + bool twoFactoRequired, bool ssoRequired, + CustomValidatorRequestContext context, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request) + { + // Arrange + request.GrantType = GrantType.ResourceOwnerPassword; + context.TwoFactorRequired = twoFactoRequired; + context.SsoRequired = ssoRequired; + if (context.User != null) + { + context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(365); + } + + AddValidDeviceToRequest(request); + + request.Raw.Add("AuthRequest", "authRequest"); + // Simulate a new device OTP being present in the request in addition to the auth request + // We don't check known device if an new device OTP is present, but we still + // want to ensure that the auth request attempt is rejected + var newDeviceOtp = "123456"; + request.Raw.Add("NewDeviceOtp", newDeviceOtp); + + // Act + var result = await _sut.ValidateRequestDeviceAsync(request, context); + + // Assert + Assert.False(result); + Assert.NotNull(context.CustomResponse["ErrorModel"]); + var expectedErrorMessage = "auth request flow unsupported on unknown device"; + var actualResponse = (ErrorResponseModel)context.CustomResponse["ErrorModel"]; + Assert.Equal(expectedErrorMessage, actualResponse.Message); + await _deviceService.Received(0).SaveAsync(Arg.Any()); + await _deviceRepository.DidNotReceive().GetByIdentifierAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -617,7 +660,7 @@ public class DeviceValidatorTests // Autodata arranges // Act - var result = DeviceValidator.NewDeviceOtpRequest(request); + var result = DeviceValidator.RequestHasNewDeviceVerificationOtp(request); // Assert Assert.False(result); @@ -631,7 +674,7 @@ public class DeviceValidatorTests request.Raw["NewDeviceOtp"] = "123456"; // Act - var result = DeviceValidator.NewDeviceOtpRequest(request); + var result = DeviceValidator.RequestHasNewDeviceVerificationOtp(request); // Assert Assert.True(result); @@ -655,7 +698,7 @@ public class DeviceValidatorTests ValidatedTokenRequest request) { context.KnownDevice = false; - request.GrantType = "password"; + request.GrantType = GrantType.ResourceOwnerPassword; context.TwoFactorRequired = false; context.SsoRequired = false; if (context.User != null) From d74c71c1d0cb8ed7269faf6db4bee8cbd4939563 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Wed, 6 Aug 2025 18:44:01 +0000 Subject: [PATCH 127/326] Fix attachments container (#6165) --- util/Attachments/Dockerfile | 4 ++-- util/Attachments/entrypoint.sh | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/util/Attachments/Dockerfile b/util/Attachments/Dockerfile index 4ab1d0c11b..5ba22918c4 100644 --- a/util/Attachments/Dockerfile +++ b/util/Attachments/Dockerfile @@ -26,7 +26,6 @@ WORKDIR /source/util/Server RUN . /tmp/rid.txt && dotnet restore -r $RID # Build project -WORKDIR /source/util/Server RUN . /tmp/rid.txt && dotnet publish \ -c release \ --no-restore \ @@ -48,7 +47,8 @@ ENV SSL_CERT_DIR=/etc/bitwarden/ca-certificates ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false EXPOSE 5000 -RUN apk add --no-cache curl \ +RUN apk add --no-cache \ + curl \ icu-libs \ shadow \ && apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu diff --git a/util/Attachments/entrypoint.sh b/util/Attachments/entrypoint.sh index 2c0942a148..0b5deaae2b 100644 --- a/util/Attachments/entrypoint.sh +++ b/util/Attachments/entrypoint.sh @@ -23,17 +23,17 @@ if [ "$(id -u)" = "0" ] then # Create user and group - addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true - adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true - mkdir -p /home/$USERNAME - chown $USERNAME:$GROUPNAME /home/$USERNAME - + groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 || + groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1 + useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 || + usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 + mkhomedir_helper $USERNAME # The rest... - chown -R $USERNAME:$GROUPNAME /bitwarden_server mkdir -p /etc/bitwarden/core/attachments chown -R $USERNAME:$GROUPNAME /etc/bitwarden + gosu_cmd="gosu $USERNAME:$GROUPNAME" else gosu_cmd="" From e61a5cc83ae5206cae1a1012cc7e81924dd2ec25 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Wed, 6 Aug 2025 14:59:53 -0500 Subject: [PATCH 128/326] PM-24509 remove limit field (#6169) --- src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index 65cb2a9fca..b93bcac7dc 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -45,9 +45,6 @@ public class RetrievalOptions [JsonPropertyName("real_time")] public bool RealTime { get; set; } = true; - - [JsonPropertyName("limit")] - public int? Limit { get; set; } = 3; } public class RetrievalOptionsRunSearch From 9d05105dc0a6c06b381d46be1a5d228b97fe9297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 7 Aug 2025 11:12:45 +0100 Subject: [PATCH 129/326] [PM-23981] Fix DefaultUserCollection filtering in organization user updates (#6161) * Refactor UpdateOrganizationUserCommand to validate and filter out DefaultUserCollections during user updates. * Enhance UpdateOrganizationUserCommandTests to filter out DefaultUserCollections during user updates, ensuring only shared collections are processed. Updated test logic to reflect new filtering behavior. * Add integration test for updating organization user with existing default collection. The test verifies successful updates to user permissions, group access, and collection access, ensuring correct handling of shared and default collections. * Refactor UpdateOrganizationUserCommand to separate the collection validation and DefaultUserCollection filtering * Refactored integration test setup/assertion for clarity --- .../UpdateOrganizationUserCommand.cs | 25 ++-- .../OrganizationUserControllerTests.cs | 128 ++++++++++++++++++ .../UpdateOrganizationUserCommandTests.cs | 38 ++++-- 3 files changed, 174 insertions(+), 17 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs index b72505c195..2623242ad6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommand.cs @@ -89,7 +89,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand if (collectionAccessList.Count != 0) { - await ValidateCollectionAccessAsync(originalOrganizationUser, collectionAccessList); + collectionAccessList = await ValidateAccessAndFilterDefaultUserCollectionsAsync(originalOrganizationUser, collectionAccessList); } if (groupAccess?.Any() == true) @@ -179,11 +179,19 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand throw new BadRequestException("User can only be an admin of one free organization."); } - private async Task ValidateCollectionAccessAsync(OrganizationUser originalUser, - ICollection collectionAccess) + private async Task> ValidateAccessAndFilterDefaultUserCollectionsAsync( + OrganizationUser originalUser, List collectionAccess) { var collections = await _collectionRepository .GetManyByManyIdsAsync(collectionAccess.Select(c => c.Id)); + + ValidateCollections(originalUser, collectionAccess, collections); + + return ExcludeDefaultUserCollections(collectionAccess, collections); + } + + private static void ValidateCollections(OrganizationUser originalUser, List collectionAccess, ICollection collections) + { var collectionIds = collections.Select(c => c.Id); var missingCollection = collectionAccess @@ -199,13 +207,14 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand // Use generic error message to avoid enumeration throw new NotFoundException(); } - - if (collections.Any(c => c.Type == CollectionType.DefaultUserCollection)) - { - throw new BadRequestException("You cannot modify member access for collections with the type as DefaultUserCollection."); - } } + private static List ExcludeDefaultUserCollections( + List collectionAccess, ICollection collections) => + collectionAccess + .Where(cas => collections.Any(c => c.Id == cas.Id && c.Type != CollectionType.DefaultUserCollection)) + .ToList(); + private async Task ValidateGroupAccessAsync(OrganizationUser originalUser, ICollection groupAccess) { diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index c60c2049a1..08ebcf5de0 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -2,8 +2,10 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; +using Bit.Api.Models.Request; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -160,12 +162,138 @@ public class OrganizationUserControllerTests : IClassFixture CreateTestDataAsync() + { + var groupRepository = _factory.GetService(); + var group = await groupRepository.CreateAsync(new Group + { + OrganizationId = _organization.Id, + Name = $"Test Group {Guid.NewGuid()}" + }); + + var collectionRepository = _factory.GetService(); + var sharedCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = _organization.Id, + Name = $"Test Collection {Guid.NewGuid()}", + Type = CollectionType.SharedCollection + }); + + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = _organization.Id, + Name = $"My Items {Guid.NewGuid()}", + Type = CollectionType.DefaultUserCollection + }); + + return (group, sharedCollection, defaultCollection); + } + + private async Task AssignDefaultCollectionToUserAsync(OrganizationUser organizationUser, Collection defaultCollection) + { + var organizationUserRepository = _factory.GetService(); + await organizationUserRepository.ReplaceAsync(organizationUser, + new List + { + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + } + }); + } + + private static OrganizationUserUpdateRequestModel CreateUpdateRequest(Collection sharedCollection, Group group) + { + return new OrganizationUserUpdateRequestModel + { + Type = OrganizationUserType.Custom, + Permissions = new Permissions + { + ManageGroups = true + }, + Collections = new List + { + new SelectionReadOnlyRequestModel + { + Id = sharedCollection.Id, + ReadOnly = true, + HidePasswords = false, + Manage = false + } + }, + Groups = new List { group.Id } + }; + } + + private async Task VerifyUserWasUpdatedCorrectlyAsync( + OrganizationUser organizationUser, + OrganizationUserType expectedType, + bool expectedManageGroups) + { + var organizationUserRepository = _factory.GetService(); + var updatedOrgUser = await organizationUserRepository.GetByIdAsync(organizationUser.Id); + Assert.NotNull(updatedOrgUser); + Assert.Equal(expectedType, updatedOrgUser.Type); + Assert.Equal(expectedManageGroups, updatedOrgUser.GetPermissions().ManageGroups); + } + + private async Task VerifyGroupAccessWasAddedAsync( + OrganizationUser organizationUser, IEnumerable groups) + { + var groupRepository = _factory.GetService(); + var userGroups = await groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id); + Assert.All(groups, group => Assert.Contains(group.Id, userGroups)); + } + + private async Task VerifyCollectionAccessWasUpdatedCorrectlyAsync( + OrganizationUser organizationUser, Guid sharedCollectionId, Guid defaultCollectionId) + { + var organizationUserRepository = _factory.GetService(); + var (_, collectionAccess) = await organizationUserRepository.GetByIdWithCollectionsAsync(organizationUser.Id); + var collectionIds = collectionAccess.Select(c => c.Id).ToHashSet(); + + Assert.Contains(defaultCollectionId, collectionIds); + Assert.Contains(sharedCollectionId, collectionIds); + + var newCollectionAccess = collectionAccess.First(c => c.Id == sharedCollectionId); + Assert.True(newCollectionAccess.ReadOnly); + Assert.False(newCollectionAccess.HidePasswords); + Assert.False(newCollectionAccess.Manage); + } + private async Task> CreateAcceptedUsersAsync(IEnumerable emails) { var acceptedUsers = new List(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs index bd112c5d40..ed8d3b2346 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/UpdateOrganizationUserCommandTests.cs @@ -245,21 +245,41 @@ public class UpdateOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task UpdateUserAsync_WithDefaultUserCollectionType_Throws(OrganizationUser user, OrganizationUser originalUser, - List collectionAccess, Guid? savingUserId, SutProvider sutProvider, - Organization organization) + public async Task UpdateUserAsync_WithMixedCollectionTypes_FiltersOutDefaultUserCollections( + OrganizationUser user, OrganizationUser originalUser, Collection sharedCollection, Collection defaultUserCollection, + Guid? savingUserId, SutProvider sutProvider, Organization organization) { + user.Permissions = null; + sharedCollection.Type = CollectionType.SharedCollection; + defaultUserCollection.Type = CollectionType.DefaultUserCollection; + sharedCollection.OrganizationId = defaultUserCollection.OrganizationId = organization.Id; + Setup(sutProvider, organization, user, originalUser); - // Return collections with DefaultUserCollection type + var collectionAccess = new List + { + new() { Id = sharedCollection.Id, ReadOnly = true, HidePasswords = false, Manage = false }, + new() { Id = defaultUserCollection.Id, ReadOnly = false, HidePasswords = true, Manage = false } + }; + sutProvider.GetDependency() .GetManyByManyIdsAsync(Arg.Any>()) - .Returns(callInfo => callInfo.Arg>() - .Select(guid => new Collection { Id = guid, OrganizationId = user.OrganizationId, Type = CollectionType.DefaultUserCollection }).ToList()); + .Returns(new List + { + new() { Id = sharedCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.SharedCollection }, + new() { Id = defaultUserCollection.Id, OrganizationId = user.OrganizationId, Type = CollectionType.DefaultUserCollection } + }); - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.UpdateUserAsync(user, OrganizationUserType.User, savingUserId, collectionAccess, null)); - Assert.Contains("You cannot modify member access for collections with the type as DefaultUserCollection.", exception.Message); + await sutProvider.Sut.UpdateUserAsync(user, OrganizationUserType.User, savingUserId, collectionAccess, null); + + // Verify that ReplaceAsync was called with only the shared collection (default user collection filtered out) + await sutProvider.GetDependency().Received(1).ReplaceAsync( + user, + Arg.Is>(collections => + collections.Count() == 1 && + collections.First().Id == sharedCollection.Id + ) + ); } private void Setup(SutProvider sutProvider, Organization organization, From e88c9b352597b7c631d8416f76e29c09dd68063a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 11 Aug 2025 14:28:57 +0000 Subject: [PATCH 130/326] Bumped version to 2025.8.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d8ace51f01..d8af8dc990 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.7.3 + 2025.8.0 Bit.$(MSBuildProjectName) enable From e042572cfb20bc5a89b0189f59696f1c0592f9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:36:40 +0100 Subject: [PATCH 131/326] [PM-24582] Bugfix: exclude admins and owners from default user collection creation on confirmation (#6177) * Update the OrganizationUserController integration Confirm tests to handle the Owner type * Refactor ConfirmOrganizationUserCommand to simplify side-effect handling in organization user confirmation. Update IPolicyRequirementQuery to return eligible org user IDs for policy enforcement. Update tests for method signature changes and default collection creation logic. --- .../ConfirmOrganizationUserCommand.cs | 46 +++++++------ .../Policies/IPolicyRequirementQuery.cs | 13 ++-- .../Implementations/PolicyRequirementQuery.cs | 14 ++-- .../OrganizationUserControllerTests.cs | 64 ++++++++++++------- .../ConfirmOrganizationUserCommandTests.cs | 23 ++----- .../Policies/PolicyRequirementQueryTests.cs | 34 +++++----- 6 files changed, 99 insertions(+), 95 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 0baa9c9e3a..cbedb6355d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -244,25 +244,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand .Select(d => d.Id.ToString()); } - private async Task OrganizationRequiresDefaultCollectionAsync(Guid organizationId, string defaultUserCollectionName) - { - if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) - { - return false; - } - - // Skip if no collection name provided (backwards compatibility) - if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) - { - return false; - } - - var organizationPolicyRequirement = await _policyRequirementQuery.GetByOrganizationAsync(organizationId); - - // Check if the organization requires default collections - return organizationPolicyRequirement.RequiresDefaultCollection(organizationId); - } - /// /// Handles the side effects of confirming an organization user. /// Creates a default collection for the user if the organization @@ -271,15 +252,32 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand /// The organization ID. /// The confirmed organization users. /// The encrypted default user collection name. - private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName) + private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, + IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName) { - var requiresDefaultCollections = await OrganizationRequiresDefaultCollectionAsync(organizationId, defaultUserCollectionName); - if (!requiresDefaultCollections) + if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) { return; } - var organizationUserIds = confirmedOrganizationUsers.Select(u => u.Id).ToList(); - await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, organizationUserIds, defaultUserCollectionName); + // Skip if no collection name provided (backwards compatibility) + if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) + { + return; + } + + var policyEligibleOrganizationUserIds = await _policyRequirementQuery.GetManyByOrganizationIdAsync(organizationId); + + var eligibleOrganizationUserIds = confirmedOrganizationUsers + .Where(ou => policyEligibleOrganizationUserIds.Contains(ou.Id)) + .Select(ou => ou.Id) + .ToList(); + + if (eligibleOrganizationUserIds.Count == 0) + { + return; + } + + await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs index 226347fe29..e662716142 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPolicyRequirementQuery.cs @@ -17,12 +17,11 @@ public interface IPolicyRequirementQuery Task GetAsync(Guid userId) where T : IPolicyRequirement; /// - /// Get a policy requirement for a specific organization. - /// This returns the policy requirement that represents the policy state for the entire organization. - /// It will always return a value even if there are no policies that should be enforced. - /// This should be used for organization-level policy checks. + /// Get all organization user IDs within an organization that are affected by a given policy type. + /// Respects role/status/provider exemptions via the policy factory's Enforce predicate. /// - /// The organization to check policies for. - /// The IPolicyRequirement that corresponds to the policy you want to enforce. - Task GetByOrganizationAsync(Guid organizationId) where T : IPolicyRequirement; + /// The organization to check. + /// The IPolicyRequirement that corresponds to the policy type to evaluate. + /// Organization user IDs for whom the policy applies within the organization. + Task> GetManyByOrganizationIdAsync(Guid organizationId) where T : IPolicyRequirement; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs index ba4495224c..e846e02e46 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/PolicyRequirementQuery.cs @@ -28,7 +28,8 @@ public class PolicyRequirementQuery( return requirement; } - public async Task GetByOrganizationAsync(Guid organizationId) where T : IPolicyRequirement + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + where T : IPolicyRequirement { var factory = factories.OfType>().SingleOrDefault(); if (factory is null) @@ -37,13 +38,14 @@ public class PolicyRequirementQuery( } var organizationPolicyDetails = await GetOrganizationPolicyDetails(organizationId, factory.PolicyType); - var filteredPolicies = organizationPolicyDetails - .Cast() - .Where(policyDetails => policyDetails.PolicyType == factory.PolicyType) + + var eligibleOrganizationUserIds = organizationPolicyDetails + .Where(p => p.PolicyType == factory.PolicyType) .Where(factory.Enforce) + .Select(p => p.OrganizationUserId) .ToList(); - var requirement = factory.Create(filteredPolicies); - return requirement; + + return eligibleOrganizationUserIds; } private Task> GetPolicyDetails(Guid userId) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index 08ebcf5de0..04ab72fad1 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -22,7 +22,6 @@ public class OrganizationUserControllerTests : IClassFixture (organizationUser, string.Format(testKeyFormat, index))).ToList()); - await VerifyMultipleUsersHaveDefaultCollectionsAsync(acceptedUsers); + await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(0), 1); + await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(1), 0); // Owner does not get a default collection + await VerifyDefaultCollectionCountAsync(acceptedUsers.ElementAt(2), 1); } [Fact] @@ -294,16 +320,18 @@ public class OrganizationUserControllerTests : IClassFixture> CreateAcceptedUsersAsync(IEnumerable emails) + private async Task> CreateAcceptedUsersAsync( + IEnumerable<(string email, OrganizationUserType userType)> newUsers) { var acceptedUsers = new List(); - foreach (var email in emails) + foreach (var (email, userType) in newUsers) { await _factory.LoginWithNewAccount(email); - var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, - OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted); + var acceptedOrgUser = await OrganizationTestHelpers.CreateUserAsync( + _factory, _organization.Id, email, + userType, userStatusType: OrganizationUserStatusType.Accepted); acceptedUsers.Add(acceptedOrgUser); } @@ -311,12 +339,11 @@ public class OrganizationUserControllerTests : IClassFixture(); var collections = await collectionRepository.GetManyByUserIdAsync(orgUser.UserId!.Value); - Assert.Single(collections); - Assert.Equal(_mockEncryptedString, collections.First().Name); + Assert.Equal(expectedCount, collections.Count); } private async Task VerifyUserConfirmedAsync(OrganizationUser orgUser, string expectedKey) @@ -334,15 +361,4 @@ public class OrganizationUserControllerTests : IClassFixture acceptedOrganizationUsers) - { - var collectionRepository = _factory.GetService(); - foreach (var acceptedOrganizationUser in acceptedOrganizationUsers) - { - var collections = await collectionRepository.GetManyByUserIdAsync(acceptedOrganizationUser.UserId!.Value); - Assert.Single(collections); - Assert.Equal(_mockEncryptedString, collections.First().Name); - } - } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index b0815d9f35..a8219ebcaa 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -10,7 +10,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -473,10 +472,8 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetByOrganizationAsync(organization.Id) - .Returns(new OrganizationDataOwnershipPolicyRequirement( - OrganizationDataOwnershipState.Enabled, - [organization.Id])); + .GetManyByOrganizationIdAsync(organization.Id) + .Returns(new List { orgUser.Id }); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); @@ -504,17 +501,11 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); - sutProvider.GetDependency() - .GetByOrganizationAsync(org.Id) - .Returns(new OrganizationDataOwnershipPolicyRequirement( - OrganizationDataOwnershipState.Enabled, - [org.Id])); - await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, ""); await sutProvider.GetDependency() .DidNotReceive() - .CreateAsync(Arg.Any(), Arg.Any>(), Arg.Any>()); + .CreateDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] @@ -533,15 +524,13 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); sutProvider.GetDependency() - .GetByOrganizationAsync(org.Id) - .Returns(new OrganizationDataOwnershipPolicyRequirement( - OrganizationDataOwnershipState.Enabled, - [Guid.NewGuid()])); + .GetManyByOrganizationIdAsync(org.Id) + .Returns(new List { orgUser.UserId!.Value }); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); await sutProvider.GetDependency() .DidNotReceive() - .CreateAsync(Arg.Any(), Arg.Any>(), Arg.Any>()); + .CreateDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs index da8f7319d5..8c25f70454 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirementQueryTests.cs @@ -81,11 +81,11 @@ public class PolicyRequirementQueryTests } [Theory, BitAutoData] - public async Task GetByOrganizationAsync_IgnoresOtherPolicyTypes(Guid organizationId) + public async Task GetManyByOrganizationIdAsync_IgnoresOtherPolicyTypes(Guid organizationId) { var policyRepository = Substitute.For(); - var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; - var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, UserId = Guid.NewGuid() }; + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.RequireSso, OrganizationUserId = Guid.NewGuid() }; // Force the repository to return both policies even though that is not the expected result policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg) .Returns([thisPolicy, otherPolicy]); @@ -93,20 +93,20 @@ public class PolicyRequirementQueryTests var factory = new TestPolicyRequirementFactory(_ => true); var sut = new PolicyRequirementQuery(policyRepository, [factory]); - var requirement = await sut.GetByOrganizationAsync(organizationId); + var organizationUserIds = await sut.GetManyByOrganizationIdAsync(organizationId); await policyRepository.Received(1).GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg); - Assert.Contains(thisPolicy, requirement.Policies.Cast()); - Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast()); + Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds); + Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds); } [Theory, BitAutoData] - public async Task GetByOrganizationAsync_CallsEnforceCallback(Guid organizationId) + public async Task GetManyByOrganizationIdAsync_CallsEnforceCallback(Guid organizationId) { var policyRepository = Substitute.For(); - var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; - var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, UserId = Guid.NewGuid() }; + var thisPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() }; + var otherPolicy = new OrganizationPolicyDetails { PolicyType = PolicyType.SingleOrg, OrganizationUserId = Guid.NewGuid() }; policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([thisPolicy, otherPolicy]); var callback = Substitute.For>(); @@ -115,28 +115,28 @@ public class PolicyRequirementQueryTests var factory = new TestPolicyRequirementFactory(callback); var sut = new PolicyRequirementQuery(policyRepository, [factory]); - var requirement = await sut.GetByOrganizationAsync(organizationId); + var organizationUserIds = await sut.GetManyByOrganizationIdAsync(organizationId); - Assert.Contains(thisPolicy, requirement.Policies.Cast()); - Assert.DoesNotContain(otherPolicy, requirement.Policies.Cast()); + Assert.Contains(thisPolicy.OrganizationUserId, organizationUserIds); + Assert.DoesNotContain(otherPolicy.OrganizationUserId, organizationUserIds); callback.Received()(Arg.Is(p => p == thisPolicy)); callback.Received()(Arg.Is(p => p == otherPolicy)); } [Theory, BitAutoData] - public async Task GetByOrganizationAsync_ThrowsIfNoFactoryRegistered(Guid organizationId) + public async Task GetManyByOrganizationIdAsync_ThrowsIfNoFactoryRegistered(Guid organizationId) { var policyRepository = Substitute.For(); var sut = new PolicyRequirementQuery(policyRepository, []); var exception = await Assert.ThrowsAsync(() - => sut.GetByOrganizationAsync(organizationId)); + => sut.GetManyByOrganizationIdAsync(organizationId)); Assert.Contains("No Requirement Factory found", exception.Message); } [Theory, BitAutoData] - public async Task GetByOrganizationAsync_HandlesNoPolicies(Guid organizationId) + public async Task GetManyByOrganizationIdAsync_HandlesNoPolicies(Guid organizationId) { var policyRepository = Substitute.For(); policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.SingleOrg).Returns([]); @@ -144,8 +144,8 @@ public class PolicyRequirementQueryTests var factory = new TestPolicyRequirementFactory(x => x.IsProvider); var sut = new PolicyRequirementQuery(policyRepository, [factory]); - var requirement = await sut.GetByOrganizationAsync(organizationId); + var organizationUserIds = await sut.GetManyByOrganizationIdAsync(organizationId); - Assert.Empty(requirement.Policies); + Assert.Empty(organizationUserIds); } } From 5b67abba31b6187126c98559f2be9916fa46aa9e Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 11 Aug 2025 12:08:56 -0500 Subject: [PATCH 132/326] [PM-24641] Remove prompt Id from onyx requests (#6183) --- src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs index b93bcac7dc..ba3b89e297 100644 --- a/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs +++ b/src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs @@ -14,9 +14,6 @@ public class OnyxAnswerWithCitationRequestModel [JsonPropertyName("persona_id")] public int PersonaId { get; set; } = 1; - [JsonPropertyName("prompt_id")] - public int PromptId { get; set; } = 1; - [JsonPropertyName("retrieval_options")] public RetrievalOptions RetrievalOptions { get; set; } From 3c5de319d166a1bdf3b7d1339e70ff14d03e7ef1 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:39:43 -0400 Subject: [PATCH 133/326] feat(2fa): [PM-24425] Add email on failed 2FA attempt * Added email on failed 2FA attempt. * Added tests. * Adjusted email verbiage. * Added feature flag. * Undid accidental change. * Undid unintentional change to clean up PR. * Linting * Added attempted method to email. * Changes to email templates. * Linting. * Email format changes. * Email formatting changes. --- ...mptsModel.cs => FailedAuthAttemptModel.cs} | 4 +- src/Core/Constants.cs | 1 + .../Auth/FailedTwoFactorAttempt.html.hbs | 37 ++++++ .../Auth/FailedTwoFactorAttempt.text.hbs | 18 +++ src/Core/Services/IMailService.cs | 2 + .../Implementations/HandlebarsMailService.cs | 20 ++++ .../NoopImplementations/NoopMailService.cs | 6 + .../RequestValidators/BaseRequestValidator.cs | 14 ++- .../CustomTokenRequestValidator.cs | 6 +- .../ResourceOwnerPasswordValidator.cs | 6 +- .../WebAuthnGrantValidator.cs | 6 +- .../BaseRequestValidatorTests.cs | 105 ++++++++++++++++-- .../BaseRequestValidatorTestWrapper.cs | 6 +- 13 files changed, 212 insertions(+), 19 deletions(-) rename src/Core/Auth/Models/Mail/{FailedAuthAttemptsModel.cs => FailedAuthAttemptModel.cs} (58%) create mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.text.hbs diff --git a/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs b/src/Core/Auth/Models/Mail/FailedAuthAttemptModel.cs similarity index 58% rename from src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs rename to src/Core/Auth/Models/Mail/FailedAuthAttemptModel.cs index e7b0b042a5..c67ac4a3d3 100644 --- a/src/Core/Auth/Models/Mail/FailedAuthAttemptsModel.cs +++ b/src/Core/Auth/Models/Mail/FailedAuthAttemptModel.cs @@ -1,11 +1,13 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.Enums; using Bit.Core.Models.Mail; namespace Bit.Core.Auth.Models.Mail; -public class FailedAuthAttemptsModel : NewDeviceLoggedInModel +public class FailedAuthAttemptModel : NewDeviceLoggedInModel { public string AffectedEmail { get; set; } + public TwoFactorProviderType TwoFactorType { get; set; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 08191ff356..5e54434a17 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -127,6 +127,7 @@ public static class FeatureFlagKeys public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string Otp6Digits = "pm-18612-otp-6-digits"; + public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.html.hbs new file mode 100644 index 0000000000..56052c7a0d --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.html.hbs @@ -0,0 +1,37 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + + + + + +
+ We've detected a failed login attempt +
+
+ If you're having trouble with two-step login, please visit the Help Center. +
+
+ If you did not recently try to log in, open the web app and take these immediate steps to secure your Bitwarden account: +
    +
  • Deauthorize all devices
  • +
  • Change your master password
  • +
+
+
+
+
+ Account: {{AffectedEmail}}
+ Two-Step Login Method: {{TwoFactorType}}
+ Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
+ IP Address: {{IpAddress}}
+
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.text.hbs new file mode 100644 index 0000000000..4ad5dd32a3 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempt.text.hbs @@ -0,0 +1,18 @@ +{{#>BasicTextLayout}} +We've detected a failed login attempt + +If you're having trouble with two-step login, please visit the Help Center (https://bitwarden.com/help/). + +If you did not recently try to log in, open the web app ({{{WebVaultUrl}}}) and take these immediate steps to secure your Bitwarden account: +- Deauthorize all devices +- Change your master password + +Account: {{AffectedEmail}} +Two-Step Login Method: {{TwoFactorType}} +Date: {{TheDate}} at {{TheTime}} {{TimeZone}} +IP Address: {{IpAddress}} + +{{/BasicTextLayout}} + + + diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index e5a7577770..32aaac84b7 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -3,6 +3,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.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -29,6 +30,7 @@ public interface IMailService Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); Task SendChangeEmailEmailAsync(string newEmailAddress, string token); Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose); + 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 254a0dd841..9dd2dffedf 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Mail; using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.Mail; @@ -193,6 +194,25 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip) + { + var message = CreateDefaultMessage("Failed two-step login attempt detected", email); + var model = new FailedAuthAttemptModel() + { + TheDate = utcNow.ToLongDateString(), + TheTime = utcNow.ToShortTimeString(), + TimeZone = _utcTimeZoneDisplay, + IpAddress = ip, + AffectedEmail = email, + TwoFactorType = failedType, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash + + }; + await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempt", model); + message.Category = "FailedTwoFactorAttempt"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendMasterPasswordHintEmailAsync(string email, string hint) { var message = CreateDefaultMessage("Your Master Password Hint", email); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index d8f2488088..5847aaf929 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -3,6 +3,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.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -92,6 +93,11 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip) + { + return Task.FromResult(0); + } + public Task SendWelcomeEmailAsync(User user) { return Task.FromResult(0); diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 3317e18264..5a8cb8645e 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -36,6 +36,7 @@ public abstract class BaseRequestValidator where T : class private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IAuthRequestRepository _authRequestRepository; + private readonly IMailService _mailService; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -61,7 +62,8 @@ public abstract class BaseRequestValidator where T : class ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IPolicyRequirementQuery policyRequirementQuery, - IAuthRequestRepository authRequestRepository + IAuthRequestRepository authRequestRepository, + IMailService mailService ) { _userManager = userManager; @@ -80,6 +82,7 @@ public abstract class BaseRequestValidator where T : class UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; PolicyRequirementQuery = policyRequirementQuery; _authRequestRepository = authRequestRepository; + _mailService = mailService; } protected async Task ValidateAsync(T context, ValidatedTokenRequest request, @@ -160,6 +163,7 @@ public abstract class BaseRequestValidator where T : class } else { + await SendFailedTwoFactorEmail(user, twoFactorProviderType); await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); } @@ -373,6 +377,14 @@ public abstract class BaseRequestValidator where T : class await _userRepository.ReplaceAsync(user); } + private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType) + { + if (FeatureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail)) + { + await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow, CurrentContext.IpAddress); + } + } + private async Task GetMasterPasswordPolicyAsync(User user) { // Check current context/cache to see if user is in any organizations, avoids extra DB call if not diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index c3d7908dc9..6223d8dc9c 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -46,7 +46,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); _policyRequirementQuery = Substitute.For(); _authRequestRepository = Substitute.For(); + _mailService = Substitute.For(); _sut = new BaseRequestValidatorTestWrapper( _userManager, @@ -88,7 +90,8 @@ public class BaseRequestValidatorTests _ssoConfigRepository, _userDecryptionOptionsBuilder, _policyRequirementQuery, - _authRequestRepository); + _authRequestRepository, + _mailService); } /* Logic path @@ -278,6 +281,98 @@ public class BaseRequestValidatorTests await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any()); } + [Theory, BitAutoData] + public async Task ValidateAsync_TwoFactorTokenInvalid_ShouldSendFailedTwoFactorEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + var user = requestContext.User; + + // 1 -> initial validation passes + _sut.isValid = true; + + // 2 -> enable the FailedTwoFactorEmail feature flag + _featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true); + + // 3 -> set up 2FA as required + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(true, null))); + + // 4 -> provide invalid 2FA token + tokenRequest.Raw["TwoFactorToken"] = "invalid_token"; + tokenRequest.Raw["TwoFactorProvider"] = TwoFactorProviderType.Email.ToString(); + + // 5 -> set up 2FA verification to fail + _twoFactorAuthenticationValidator + .VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Email, "invalid_token") + .Returns(Task.FromResult(false)); + + // Act + await _sut.ValidateAsync(context); + + // Assert + // Verify that the failed 2FA email was sent + await _mailService.Received(1) + .SendFailedTwoFactorAttemptEmailAsync( + user.Email, + TwoFactorProviderType.Email, + Arg.Any(), + Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_TwoFactorRememberTokenExpired_ShouldNotSendFailedTwoFactorEmail( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + var user = requestContext.User; + + // 1 -> initial validation passes + _sut.isValid = true; + + // 2 -> enable the FailedTwoFactorEmail feature flag + _featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail).Returns(true); + + // 3 -> set up 2FA as required + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), tokenRequest) + .Returns(Task.FromResult(new Tuple(true, null))); + + // 4 -> provide invalid remember token (remember token expired) + tokenRequest.Raw["TwoFactorToken"] = "expired_remember_token"; + tokenRequest.Raw["TwoFactorProvider"] = "5"; // Remember provider + + // 5 -> set up remember token verification to fail + _twoFactorAuthenticationValidator + .VerifyTwoFactorAsync(user, null, TwoFactorProviderType.Remember, "expired_remember_token") + .Returns(Task.FromResult(false)); + + // 6 -> set up dummy BuildTwoFactorResultAsync + var twoFactorResultDict = new Dictionary + { + { "TwoFactorProviders", new[] { "0", "1" } }, + { "TwoFactorProviders2", new Dictionary() } + }; + _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, null) + .Returns(Task.FromResult(twoFactorResultDict)); + + // Act + await _sut.ValidateAsync(context); + + // Assert + // Verify that the failed 2FA email was NOT sent for remember token expiration + await _mailService.DidNotReceive() + .SendFailedTwoFactorAttemptEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + // Test grantTypes that require SSO when a user is in an organization that requires it [Theory] [BitAutoData("password")] @@ -600,12 +695,4 @@ public class BaseRequestValidatorTests Substitute.For(), Substitute.For>>()); } - - private void AddValidDeviceToRequest(ValidatedTokenRequest request) - { - request.Raw["DeviceIdentifier"] = "DeviceIdentifier"; - request.Raw["DeviceType"] = "Android"; // must be valid device type - request.Raw["DeviceName"] = "DeviceName"; - request.Raw["DevicePushToken"] = "DevicePushToken"; - } } diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 140e171309..db3deedf02 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -63,7 +63,8 @@ IBaseRequestValidatorTestWrapper ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IPolicyRequirementQuery policyRequirementQuery, - IAuthRequestRepository authRequestRepository) : + IAuthRequestRepository authRequestRepository, + IMailService mailService) : base( userManager, userService, @@ -80,7 +81,8 @@ IBaseRequestValidatorTestWrapper ssoConfigRepository, userDecryptionOptionsBuilder, policyRequirementQuery, - authRequestRepository) + authRequestRepository, + mailService) { } From 9022ad23607225cefa84ee5567a3e62ecc45f4cb Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:21:29 +1000 Subject: [PATCH 134/326] [PM-20140] Prevent accidental bulk removal of users without a Master Password (#6173) --- ...ImportOrganizationUsersAndGroupsCommand.cs | 43 ++++++++++--------- src/Core/Constants.cs | 1 + ...OrganizationServiceCollectionExtensions.cs | 1 + ...tOrganizationUsersAndGroupsCommandTests.cs | 34 ++++++++++++++- .../Helpers/OrganizationTestHelpers.cs | 30 +++++++++++-- ...tOrganizationUsersAndGroupsCommandTests.cs | 40 ++++++++++++++--- 6 files changed, 118 insertions(+), 31 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs index 89288eb4ba..87c6ddea6f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs @@ -1,12 +1,7 @@ -#nullable enable - -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Pricing; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -17,7 +12,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +namespace Bit.Core.AdminConsole.OrganizationFeatures.Import; public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersAndGroupsCommand { @@ -26,10 +21,8 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA private readonly IPaymentService _paymentService; private readonly IGroupRepository _groupRepository; private readonly IEventService _eventService; - private readonly ICurrentContext _currentContext; private readonly IOrganizationService _organizationService; - private readonly IInviteOrganizationUsersCommand _inviteOrganizationUsersCommand; - private readonly IPricingClient _pricingClient; + private readonly IFeatureService _featureService; private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi; @@ -38,21 +31,16 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA IPaymentService paymentService, IGroupRepository groupRepository, IEventService eventService, - ICurrentContext currentContext, IOrganizationService organizationService, - IInviteOrganizationUsersCommand inviteOrganizationUsersCommand, - IPricingClient pricingClient - ) + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _paymentService = paymentService; _groupRepository = groupRepository; _eventService = eventService; - _currentContext = currentContext; _organizationService = organizationService; - _inviteOrganizationUsersCommand = inviteOrganizationUsersCommand; - _pricingClient = pricingClient; + _featureService = featureService; } /// @@ -243,10 +231,23 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)> events, OrganizationUserImportData importUserData) { - var usersToDelete = importUserData.ExistingExternalUsers.Where(u => - u.Type != OrganizationUserType.Owner && - !importUserData.ImportedExternalIds.Contains(u.ExternalId) && - importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId)); + var usersToDelete = importUserData.ExistingExternalUsers + .Where(u => + u.Type != OrganizationUserType.Owner && + !importUserData.ImportedExternalIds.Contains(u.ExternalId) && + importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId)) + .ToList(); + + if (_featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) && + usersToDelete.Any(u => !u.HasMasterPassword)) + { + // Removing users without an MP will put their account in an unrecoverable state. + // We allow this during normal syncs for offboarding, but overwriteExisting risks bricking every user in + // the organization, so you don't get to do it here. + throw new BadRequestException( + "Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again."); + } + await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); events.AddRange(usersToDelete.Select(u => ( u, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5e54434a17..be82d9fc21 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -116,6 +116,7 @@ public static class FeatureFlagKeys public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; public const string MembersGetEndpointOptimization = "pm-23113-optimize-get-members-endpoint"; + public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 998354b9f8..234d6f1a84 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.OrganizationAuth.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Import; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections; diff --git a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs index 5c29b8b1b7..f04fb62c1a 100644 --- a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -26,8 +26,13 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor) - .Returns(true)); + => + { + featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor) + .Returns(true); + featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) + .Returns(true); + }); _client = _factory.CreateClient(); _loginHelper = new LoginHelper(_factory, _client); } @@ -309,4 +314,29 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture(); @@ -78,7 +79,7 @@ public static class OrganizationTestHelpers Key = null, Type = type, Status = userStatusType, - ExternalId = null, + ExternalId = externalId, AccessSecretsManager = accessSecretsManager, Email = userEmail }; @@ -110,7 +111,7 @@ public static class OrganizationTestHelpers await factory.LoginWithNewAccount(email); // Create organizationUser - var organizationUser = await OrganizationTestHelpers.CreateUserAsync(factory, organizationId, email, userType, + var organizationUser = await CreateUserAsync(factory, organizationId, email, userType, permissions: permissions); return (email, organizationUser); @@ -168,4 +169,27 @@ public static class OrganizationTestHelpers await policyRepository.CreateAsync(policy); } + + /// + /// Creates a user account without a Master Password and adds them as a member to the specified organization. + /// + public static async Task<(User User, OrganizationUser OrganizationUser)> CreateUserWithoutMasterPasswordAsync(ApiApplicationFactory factory, string email, Guid organizationId) + { + var userRepository = factory.GetService(); + var user = await userRepository.CreateAsync(new User + { + Email = email, + Culture = "en-US", + SecurityStamp = "D7ZH62BWAZ5R5CASKULCDDIQGKDA2EJ6", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwMj7W00xS7H0NWasGn7PfEq8VfH3fa5XuZucsKxLLRAHHZk0xGRZJH2lFIznizv3GpF8vzhHhe9VpmMkrdIa5oWhwHpy+D7Z1QCQxuUXzvMKpa95GOntr89nN/mWKpk6abjgjmDcqFJ0lhDqkKnDfes+d8BBd5oEA8p41/Ykz7OfG7AiktVBpTQFW09MQh1NOvcLxVgiUUVRPwNRKrOeCekWDtOjZhASMETv3kI1ogvhHukOQ3ztDzrxvmwnLQ+cXl1EeD8gQnGDp3QLiJqxPgh2EdmANh4IzjRexoDn6BqhRGqLLIoLAbbkoiNrd6NYujrWW0N8KMMoVEXuJL2g4wIDAQAB", + PrivateKey = "2.Ytudv+Qk3ET9hN8whqpuGg==|ijsFhmjaf1aaT9uz+IPhVTzMS+2W/ldAP8LdT5VyJaFdx4HSdLcWSZvz5xWuuW94zfv1Qh+p3iQIuZOr29G4jcx47rYtz4ssiFtB7Ia552ZeF+cb7uuVg40CIe7ycuJQITk00o8gots+wFnaEvk0Vjgycnqutm0jpeBJ1joWJWqTVgSsYdUGLu7PiJywQ9NgY4+bJXqadlcviS3rhPKJXtiXYJhqJqSw+vI0Yxp96MJ0HcFJk/LG22YJPTvL5kzuDq/Wzj40kj8blQ+ag+xHD4P/KJ/MppEB3OpDw3UoJ50Ek+YB9pOqGxZtvqMEzBDsgh0yoz1O992UnhaUqtJ5e9Bxy3PA6cJsdyn9npduNOreEb8vePCidN2XC+chjJpPFpjms9muHLKgfaTIfpiJA2Tz8E9dvSyhHHTE1mY+xEA7P08BYKN3LNoSGIjdiZuouJ1V/KZvCssDfVG1tli2qpnhTIh4m3rAMhbM8WW3B7wCV8N0MpcJJSvndkVcMgRbgWcbivLeXuKdE/K98n01RvOLSJyslhLGCGEQQKw6N3HQ2iELfv84YQZi2fjDK+OqAmXDq1pNcjKX2I8dqBwl31tPC8qSZiWnfinwLdqQTvSQjOIyAHb4sSjAwgdMbCRzUTChRr09l+PAZqGWdMC5N2Bw+bA8WP0l2Wdxuv9Abxl3F7xGeAA9Rw9PU5wGKujaMRmO4V9MFjNyyCcw4D9pzKMW6OUKsHsHE7tsG7KskCzksHzrZGawAt0S41BYQA/JwePCrD3F6dM92anlC1LfA00KJb0tmFdU0yJNmJfR+S78yn8yM6wDgIs2cFB3W1fYfpfUvQm+zzPoEQihNxBxnwFsBtMAOtPy54FjSzKmxsQTrYT9E6NFb8k6ZIIm2gNeOPK9OUJgjw+4g2BXErM6ikHTzM3xcaTq/cQaePZ52emndw1qOtdV06hr2EeuLM8frfLHpsknUe8JeYeW5p9E8QdZjjSN9034usdYNamUdxzmn/Mw/ar8z1xSKS6zcaQoTQ7aYLEX3dWJndc4W64HyiaRkLjO6qLUFeOerfz5UvcxxRY89eAA0KLC2xnGkBMOhXxYzIB3lF8Zxqb4JMhoBGw1n31TDfhRDGDHHEAsZuAIcH7aC5RDVxU08Jxmw4oLmeTDZA5BFcqp2A3fusNVZUnfpmMy6DCJyFprlRl8jSlJMAvhbxVuuLFDZnjl77Z2of796Ur6DgmNwYtMPNEntZPIcZ76VPLWAL8lqiRBm20c4qiwr5rNSr5kry9bR1EfXHwFRjy5pxFQ+5+ilpRl8WPfT/iUuORd8J2wnCmghm7uxiJd9t82kX0s6benhL29dQ1etqt5soX2RnlfKan16GVWoI3xrljIQrCAY4xpdptSpglOnrpSClbN1nhGkDfFPNq2pWhQrDbznDknAJ9MxQaVnLYPhn7I849GMd7EvpSkydwQu7QXn9+H4jxn6UEntNGxcL0xkG+xippvZEe+HBvcDD40efDQW1bDbILLjPb4rNRx4d3xaQnVNaF7L33osm5LgfXAQSwHJiURdkU4zmhtPP4zn0br0OdFlR3mPcrkeNeSvs7FxiKtD6n6s+av+4bKjbLL1OyuwmTnMilL6p+m8ldte0yos/r+zOuxWeI=|euhiXWXehYbFQhlAV6LIECSIPCIRaHbNdr9OI4cTPUM=", + ApiKey = "CfGrD4MoJu3NprOBZNL8tu5ocmtnmU", + KdfIterations = 600000 + }); + + var organizationUser = await CreateUserAsync(factory, organizationId, user.Email, + OrganizationUserType.User, externalId: email); + + return (user, organizationUser); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs index da02fbcf4d..bff1af1cde 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -1,9 +1,9 @@ using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.Import; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -18,11 +18,10 @@ using NSubstitute; using Xunit; using Organization = Bit.Core.AdminConsole.Entities.Organization; -namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Import; public class ImportOrganizationUsersAndGroupsCommandTests { - private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); [Theory, PaidOrganizationCustomize, BitAutoData] @@ -66,7 +65,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests Users = existingUsers.Count, Sponsored = 0 }); - sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); sutProvider.GetDependency().InviteUsersAsync(org.Id, Guid.Empty, EventSystemUser.PublicApi, Arg.Any>()) .Returns(orgUsers); @@ -92,6 +90,38 @@ public class ImportOrganizationUsersAndGroupsCommandTests .LogOrganizationUserEventsAsync(Arg.Any>()); } + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task OverwriteExistingUsers_WhenRemovingUserWithoutMasterPassword_Throws( + SutProvider sutProvider, + Organization org, List existingUsers) + { + SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []); + + // Existing user does not have a master password + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) + .Returns(true); + existingUsers.First().HasMasterPassword = false; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id).Returns(existingUsers); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ImportAsync(org.Id, [], [], [], true)); + + Assert.Contains("Sync failed. To proceed, disable the 'Remove and re-add users during next sync' setting and try again.", exception.Message); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .InviteUsersAsync(default, default, default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventsAsync(Arg.Any>()); + } + [Theory, PaidOrganizationCustomize, BitAutoData] public async Task OrgImportCreateNewUsersAndMarryExistingUser( SutProvider sutProvider, From f88baba66b0444c0e1c6a5e53634b5b936b63ed0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 13 Aug 2025 08:23:22 -0500 Subject: [PATCH 135/326] [PM-23580] Security Task Metrics (#6164) * add metrics endpoint for an organization to return completed and total security tasks * refactor metrics fetch to use sql sproc for efficiency rather than having to pull all security task data * add separate response model for security task metrics endpoint * Pascal Case to match existing implementations * refactor org to organization for consistency with other methods * alter security task endpoint: - remove "count" from variable naming - update sproc naming * remove enablement check * replace orgId with organizationId --- .../Controllers/SecurityTaskController.cs | 17 +++- .../SecurityTaskMetricsResponseModel.cs | 21 +++++ .../Vault/Entities/SecurityTaskMetrics.cs | 13 +++ .../GetTaskMetricsForOrganizationQuery.cs | 42 ++++++++++ .../IGetTaskMetricsForOrganizationQuery.cs | 13 +++ .../Repositories/ISecurityTaskRepository.cs | 7 ++ .../Vault/VaultServiceCollectionExtensions.cs | 1 + .../Repositories/SecurityTaskRepository.cs | 13 +++ .../Repositories/SecurityTaskRepository.cs | 20 +++++ ...curityTask_ReadMetricsByOrganizationId.sql | 14 ++++ .../SecurityTaskRepositoryTests.cs | 79 +++++++++++++++++++ .../2025-08-12_00_SecurityTaskMetrics.sql | 15 ++++ 12 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/Api/Vault/Models/Response/SecurityTaskMetricsResponseModel.cs create mode 100644 src/Core/Vault/Entities/SecurityTaskMetrics.cs create mode 100644 src/Core/Vault/Queries/GetTaskMetricsForOrganizationQuery.cs create mode 100644 src/Core/Vault/Queries/IGetTaskMetricsForOrganizationQuery.cs create mode 100644 src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadMetricsByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2025-08-12_00_SecurityTaskMetrics.sql diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 7f61271ab2..efff200e86 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -24,6 +24,7 @@ public class SecurityTaskController : Controller private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery; private readonly ICreateManyTasksCommand _createManyTasksCommand; private readonly ICreateManyTaskNotificationsCommand _createManyTaskNotificationsCommand; + private readonly IGetTaskMetricsForOrganizationQuery _getTaskMetricsForOrganizationQuery; public SecurityTaskController( IUserService userService, @@ -31,7 +32,8 @@ public class SecurityTaskController : Controller IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, IGetTasksForOrganizationQuery getTasksForOrganizationQuery, ICreateManyTasksCommand createManyTasksCommand, - ICreateManyTaskNotificationsCommand createManyTaskNotificationsCommand) + ICreateManyTaskNotificationsCommand createManyTaskNotificationsCommand, + IGetTaskMetricsForOrganizationQuery getTaskMetricsForOrganizationQuery) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; @@ -39,6 +41,7 @@ public class SecurityTaskController : Controller _getTasksForOrganizationQuery = getTasksForOrganizationQuery; _createManyTasksCommand = createManyTasksCommand; _createManyTaskNotificationsCommand = createManyTaskNotificationsCommand; + _getTaskMetricsForOrganizationQuery = getTaskMetricsForOrganizationQuery; } /// @@ -80,6 +83,18 @@ public class SecurityTaskController : Controller return new ListResponseModel(response); } + /// + /// Retrieves security task metrics for an organization. + /// + /// The organization Id + [HttpGet("{organizationId:guid}/metrics")] + public async Task GetTaskMetricsForOrganization([FromRoute] Guid organizationId) + { + var metrics = await _getTaskMetricsForOrganizationQuery.GetTaskMetrics(organizationId); + + return new SecurityTaskMetricsResponseModel(metrics.CompletedTasks, metrics.TotalTasks); + } + /// /// Bulk create security tasks for an organization. /// diff --git a/src/Api/Vault/Models/Response/SecurityTaskMetricsResponseModel.cs b/src/Api/Vault/Models/Response/SecurityTaskMetricsResponseModel.cs new file mode 100644 index 0000000000..502e90ddea --- /dev/null +++ b/src/Api/Vault/Models/Response/SecurityTaskMetricsResponseModel.cs @@ -0,0 +1,21 @@ +namespace Bit.Api.Vault.Models.Response; + +public class SecurityTaskMetricsResponseModel +{ + + public SecurityTaskMetricsResponseModel(int completedTasks, int totalTasks) + { + CompletedTasks = completedTasks; + TotalTasks = totalTasks; + } + + /// + /// Number of tasks that have been completed in the organization. + /// + public int CompletedTasks { get; set; } + + /// + /// Total number of tasks in the organization, regardless of their status. + /// + public int TotalTasks { get; set; } +} diff --git a/src/Core/Vault/Entities/SecurityTaskMetrics.cs b/src/Core/Vault/Entities/SecurityTaskMetrics.cs new file mode 100644 index 0000000000..c4172f6af9 --- /dev/null +++ b/src/Core/Vault/Entities/SecurityTaskMetrics.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Vault.Entities; + +public class SecurityTaskMetrics +{ + public SecurityTaskMetrics(int completedTasks, int totalTasks) + { + CompletedTasks = completedTasks; + TotalTasks = totalTasks; + } + + public int CompletedTasks { get; set; } + public int TotalTasks { get; set; } +} diff --git a/src/Core/Vault/Queries/GetTaskMetricsForOrganizationQuery.cs b/src/Core/Vault/Queries/GetTaskMetricsForOrganizationQuery.cs new file mode 100644 index 0000000000..f51efe6274 --- /dev/null +++ b/src/Core/Vault/Queries/GetTaskMetricsForOrganizationQuery.cs @@ -0,0 +1,42 @@ +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.Vault.Queries; + +public class GetTaskMetricsForOrganizationQuery : IGetTaskMetricsForOrganizationQuery +{ + private readonly ISecurityTaskRepository _securityTaskRepository; + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + + public GetTaskMetricsForOrganizationQuery( + ISecurityTaskRepository securityTaskRepository, + IAuthorizationService authorizationService, + ICurrentContext currentContext + ) + { + _securityTaskRepository = securityTaskRepository; + _authorizationService = authorizationService; + _currentContext = currentContext; + } + + public async Task GetTaskMetrics(Guid organizationId) + { + var organization = _currentContext.GetOrganization(organizationId); + var userId = _currentContext.UserId; + + if (organization == null || !userId.HasValue) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, organization, SecurityTaskOperations.ListAllForOrganization); + + return await _securityTaskRepository.GetTaskMetricsAsync(organizationId); + } +} diff --git a/src/Core/Vault/Queries/IGetTaskMetricsForOrganizationQuery.cs b/src/Core/Vault/Queries/IGetTaskMetricsForOrganizationQuery.cs new file mode 100644 index 0000000000..49054e484d --- /dev/null +++ b/src/Core/Vault/Queries/IGetTaskMetricsForOrganizationQuery.cs @@ -0,0 +1,13 @@ +using Bit.Core.Vault.Entities; + +namespace Bit.Core.Vault.Queries; + +public interface IGetTaskMetricsForOrganizationQuery +{ + /// + /// Retrieves security task metrics for an organization. + /// + /// The Id of the organization + /// Metrics for all security tasks within an organization. + Task GetTaskMetrics(Guid organizationId); +} diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index cc8303345d..4b88f1c0e8 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -28,4 +28,11 @@ public interface ISecurityTaskRepository : IRepository /// Collection of tasks to create /// Collection of created security tasks Task> CreateManyAsync(IEnumerable tasks); + + /// + /// Retrieves security task metrics for an organization. + /// + /// The id of the organization + /// A collection of security task metrics + Task GetTaskMetricsAsync(Guid organizationId); } diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 9efa1ea379..1acc74959d 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -25,5 +25,6 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index f7a5f3b878..292e99d6ad 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -48,6 +48,19 @@ public class SecurityTaskRepository : Repository, ISecurityT return results.ToList(); } + /// + public async Task GetTaskMetricsAsync(Guid organizationId) + { + await using var connection = new SqlConnection(ConnectionString); + + var result = await connection.QueryAsync( + $"[{Schema}].[SecurityTask_ReadMetricsByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return result.FirstOrDefault() ?? new SecurityTaskMetrics(0, 0); + } + /// public async Task> CreateManyAsync(IEnumerable tasks) { diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index a3ba2632fe..d4f9424d40 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -76,4 +76,24 @@ public class SecurityTaskRepository : Repository + public async Task GetTaskMetricsAsync(Guid organizationId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var metrics = await (from st in dbContext.SecurityTasks + join o in dbContext.Organizations on st.OrganizationId equals o.Id + where st.OrganizationId == organizationId && o.Enabled + select st) + .GroupBy(x => 1) + .Select(g => new Core.Vault.Entities.SecurityTaskMetrics( + g.Count(x => x.Status == SecurityTaskStatus.Completed), + g.Count() + )) + .FirstOrDefaultAsync(); + + return metrics ?? new Core.Vault.Entities.SecurityTaskMetrics(0, 0); + } } diff --git a/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadMetricsByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadMetricsByOrganizationId.sql new file mode 100644 index 0000000000..0d9d076a98 --- /dev/null +++ b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadMetricsByOrganizationId.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[SecurityTask_ReadMetricsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(CASE WHEN st.[Status] = 1 THEN 1 END) AS CompletedTasks, + COUNT(*) AS TotalTasks + FROM + [dbo].[SecurityTaskView] st + WHERE + st.[OrganizationId] = @OrganizationId +END diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs index eb5a310db3..f17950c04d 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs @@ -266,4 +266,83 @@ public class SecurityTaskRepositoryTests Assert.Equal(2, taskIds.Count); } + + [DatabaseTheory, DatabaseData] + public async Task GetTaskMetricsAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "" + }); + + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher1); + + var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher2); + + var tasks = new List + { + new() + { + OrganizationId = organization.Id, + CipherId = cipher1.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }, + new() + { + OrganizationId = organization.Id, + CipherId = cipher1.Id, + Status = SecurityTaskStatus.Completed, + Type = SecurityTaskType.UpdateAtRiskCredential, + }, + new() + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Completed, + Type = SecurityTaskType.UpdateAtRiskCredential, + }, + new() + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + } + }; + + await securityTaskRepository.CreateManyAsync(tasks); + + var metrics = await securityTaskRepository.GetTaskMetricsAsync(organization.Id); + + Assert.Equal(2, metrics.CompletedTasks); + Assert.Equal(4, metrics.TotalTasks); + } + + [DatabaseTheory, DatabaseData] + public async Task GetZeroTaskMetricsAsync( + IOrganizationRepository organizationRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "" + }); + + var metrics = await securityTaskRepository.GetTaskMetricsAsync(organization.Id); + + Assert.Equal(0, metrics.CompletedTasks); + Assert.Equal(0, metrics.TotalTasks); + } } diff --git a/util/Migrator/DbScripts/2025-08-12_00_SecurityTaskMetrics.sql b/util/Migrator/DbScripts/2025-08-12_00_SecurityTaskMetrics.sql new file mode 100644 index 0000000000..81d5c267a7 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-12_00_SecurityTaskMetrics.sql @@ -0,0 +1,15 @@ +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadMetricsByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(CASE WHEN st.[Status] = 1 THEN 1 END) AS CompletedTasks, + COUNT(*) AS TotalTasks + FROM + [dbo].[SecurityTaskView] st + WHERE + st.[OrganizationId] = @OrganizationId +END +GO From 87877aeb3da7c8b953f89abf0a6fbb7325e4f23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:01:51 +0100 Subject: [PATCH 136/326] [PM-24414] Remove CollectionType property from the public CollectionResponseModel (#6180) --- src/Api/Models/Public/Response/CollectionResponseModel.cs | 6 ------ .../Public/Controllers/CollectionsControllerTests.cs | 1 - 2 files changed, 7 deletions(-) diff --git a/src/Api/Models/Public/Response/CollectionResponseModel.cs b/src/Api/Models/Public/Response/CollectionResponseModel.cs index 2d13f982cd..04ae565a27 100644 --- a/src/Api/Models/Public/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Public/Response/CollectionResponseModel.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Data; namespace Bit.Api.Models.Public.Response; @@ -24,7 +23,6 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel Id = collection.Id; ExternalId = collection.ExternalId; Groups = groups?.Select(c => new AssociationWithPermissionsResponseModel(c)); - Type = collection.Type; } /// @@ -43,8 +41,4 @@ public class CollectionResponseModel : CollectionBaseModel, IResponseModel /// The associated groups that this collection is assigned to. /// public IEnumerable Groups { get; set; } - /// - /// The type of this collection - /// - public CollectionType Type { get; set; } } diff --git a/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs index d896fc9c74..9cfafbac54 100644 --- a/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs @@ -67,7 +67,6 @@ public class CollectionsControllerTests var jsonResult = Assert.IsType(result); var response = Assert.IsType(jsonResult.Value); Assert.Equal(collection.Id, response.Id); - Assert.Equal(collection.Type, response.Type); } [Theory, BitAutoData] From 43d753dcb1d9dc9ead7f40ba30c5f1d7ba70c5c2 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 13 Aug 2025 18:38:00 -0400 Subject: [PATCH 137/326] [PM-20592] [PM-22737] [PM-22738] Send grant validator (#6151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **feat**: create `SendGrantValidator` and initial `SendPasswordValidator` for Send access grants **feat**: add feature flag to toggle Send grant validation logic **feat**: add Send client to Identity and update `ApiClient` to generic `Client` **feat**: register Send services in DI pipeline **feat**: add claims management support to `ProfileService` **feat**: distinguish between invalid grant and invalid request in `SendAccessGrantValidator` **fix**: update parsing of `send_id` from request **fix**: add early return when feature flag is disabled **fix**: rename and organize Send access scope and grant type **fix**: dotnet format **test**: add unit and integration tests for `SendGrantValidator` **test**: update OpenID configuration and API resource claims **doc**: move documentation to interfaces and update inline comments **chore**: add TODO for future support of `CustomGrantTypes` --- .../IdentityServer/StaticClientStoreTests.cs | 2 +- src/Core/Constants.cs | 1 + src/Core/Enums/BitwardenClient.cs | 3 +- src/Core/Identity/Claims.cs | 2 + src/Core/Identity/IdentityClientType.cs | 1 + src/Core/IdentityServer/ApiScopes.cs | 2 + ...eyManagementServiceCollectionExtensions.cs | 1 + .../Sends/ISendPasswordHasher.cs | 1 - src/Core/Settings/GlobalSettings.cs | 1 + src/Identity/IdentityServer/ApiResources.cs | 6 +- .../IdentityServer/DynamicClientStore.cs | 2 +- .../IdentityServer/Enums/CustomGrantTypes.cs | 11 + src/Identity/IdentityServer/ProfileService.cs | 41 ++- .../Enums/SendGrantValidatorResultTypes.cs | 11 + .../Enums/SendPasswordValidatorResultTypes.cs | 9 + .../ISendPasswordRequestValidator.cs | 16 + .../SendAccess/SendAccessGrantValidator.cs | 150 ++++++++ .../SendPasswordRequestValidator.cs | 67 ++++ .../IdentityServer/StaticClientStore.cs | 8 +- .../StaticClients/SendClientBuilder.cs | 31 ++ .../Utilities/ServiceCollectionExtensions.cs | 5 +- ...endAccessGrantValidatorIntegrationTests.cs | 271 ++++++++++++++ .../openid-configuration.json | 5 +- .../SendAccessGrantValidatorTests.cs | 333 ++++++++++++++++++ 24 files changed, 961 insertions(+), 19 deletions(-) create mode 100644 src/Identity/IdentityServer/Enums/CustomGrantTypes.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs create mode 100644 src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs create mode 100644 test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs create mode 100644 test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs diff --git a/perf/MicroBenchmarks/Identity/IdentityServer/StaticClientStoreTests.cs b/perf/MicroBenchmarks/Identity/IdentityServer/StaticClientStoreTests.cs index 9d88f960ea..fcdda22c10 100644 --- a/perf/MicroBenchmarks/Identity/IdentityServer/StaticClientStoreTests.cs +++ b/perf/MicroBenchmarks/Identity/IdentityServer/StaticClientStoreTests.cs @@ -20,7 +20,7 @@ public class StaticClientStoreTests [Benchmark] public Client? TryGetValue() { - return _store.ApiClients.TryGetValue(ClientId, out var client) + return _store.Clients.TryGetValue(ClientId, out var client) ? client : null; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index be82d9fc21..1ba80d3c9b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -191,6 +191,7 @@ public static class FeatureFlagKeys 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"; /* Platform Team */ public const string PersistPopupView = "persist-popup-view"; diff --git a/src/Core/Enums/BitwardenClient.cs b/src/Core/Enums/BitwardenClient.cs index 6a1244c0c4..4776e0de3f 100644 --- a/src/Core/Enums/BitwardenClient.cs +++ b/src/Core/Enums/BitwardenClient.cs @@ -8,5 +8,6 @@ public static class BitwardenClient Desktop = "desktop", Mobile = "mobile", Cli = "cli", - DirectoryConnector = "connector"; + DirectoryConnector = "connector", + Send = "send"; } diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs index fad7b37b5f..ef3d5e450c 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Identity/Claims.cs @@ -39,4 +39,6 @@ public static class Claims public const string ManageResetPassword = "manageresetpassword"; public const string ManageScim = "managescim"; } + + public const string SendId = "send_id"; } diff --git a/src/Core/Identity/IdentityClientType.cs b/src/Core/Identity/IdentityClientType.cs index bd5b68ff6f..9c43007f25 100644 --- a/src/Core/Identity/IdentityClientType.cs +++ b/src/Core/Identity/IdentityClientType.cs @@ -5,4 +5,5 @@ public enum IdentityClientType : byte User = 0, Organization = 1, ServiceAccount = 2, + Send = 3 } diff --git a/src/Core/IdentityServer/ApiScopes.cs b/src/Core/IdentityServer/ApiScopes.cs index 6e3ce0d140..77ccb5a58a 100644 --- a/src/Core/IdentityServer/ApiScopes.cs +++ b/src/Core/IdentityServer/ApiScopes.cs @@ -11,6 +11,7 @@ public static class ApiScopes public const string ApiPush = "api.push"; public const string ApiSecrets = "api.secrets"; public const string Internal = "internal"; + public const string ApiSendAccess = "api.send.access"; public static IEnumerable GetApiScopes() { @@ -23,6 +24,7 @@ public static class ApiScopes new(ApiInstallation, "API Installation Access"), new(Internal, "Internal Access"), new(ApiSecrets, "Secrets Manager Access"), + new(ApiSendAccess, "API Send Access"), }; } } diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index 102630c7e6..cacf3d4140 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ public static class KeyManagementServiceCollectionExtensions public static void AddKeyManagementServices(this IServiceCollection services) { services.AddKeyManagementCommands(); + services.AddSendPasswordServices(); } private static void AddKeyManagementCommands(this IServiceCollection services) diff --git a/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs b/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs index 63bb7f5499..b84be5abc0 100644 --- a/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs +++ b/src/Core/KeyManagement/Sends/ISendPasswordHasher.cs @@ -9,7 +9,6 @@ public interface ISendPasswordHasher /// The send password that is hashed by the server. /// The user provided password hash that has not yet been hashed by the server for comparison. /// true if hashes match false otherwise - /// Thrown if the server password hash or client password hash is null or empty. bool PasswordHashMatches(string sendPasswordHash, string clientPasswordHash); /// diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 2d9301e451..d6e18a4c81 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -89,6 +89,7 @@ public class GlobalSettings : IGlobalSettings public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings(); public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings(); + public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5; public virtual bool EnableEmailVerification { get; set; } public virtual string KdfDefaultHashKey { get; set; } public virtual string PricingUri { get; set; } diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index f969d67908..a195f01bff 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -25,8 +25,12 @@ public class ApiResources Claims.OrganizationCustom, Claims.ProviderAdmin, Claims.ProviderServiceUser, - Claims.SecretsManagerAccess, + Claims.SecretsManagerAccess }), + new(ApiScopes.ApiSendAccess, [ + JwtClaimTypes.Subject, + Claims.SendId + ]), new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }), new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }), new(ApiScopes.ApiLicensing, new[] { JwtClaimTypes.Subject }), diff --git a/src/Identity/IdentityServer/DynamicClientStore.cs b/src/Identity/IdentityServer/DynamicClientStore.cs index 9d7764bf42..d7e589a093 100644 --- a/src/Identity/IdentityServer/DynamicClientStore.cs +++ b/src/Identity/IdentityServer/DynamicClientStore.cs @@ -37,7 +37,7 @@ internal class DynamicClientStore : IClientStore if (firstPeriod == -1) { // No splitter, attempt but don't fail for a static client - if (_staticClientStore.ApiClients.TryGetValue(clientId, out var client)) + if (_staticClientStore.Clients.TryGetValue(clientId, out var client)) { return Task.FromResult(client); } diff --git a/src/Identity/IdentityServer/Enums/CustomGrantTypes.cs b/src/Identity/IdentityServer/Enums/CustomGrantTypes.cs new file mode 100644 index 0000000000..7203386bc5 --- /dev/null +++ b/src/Identity/IdentityServer/Enums/CustomGrantTypes.cs @@ -0,0 +1,11 @@ +namespace Bit.Identity.IdentityServer.Enums; + +/// +/// A class containing custom grant types used in the Bitwarden IdentityServer implementation +/// +public static class CustomGrantTypes +{ + public const string SendAccess = "send_access"; + // TODO: PM-24471 replace magic string with a constant for webauthn + public const string WebAuthn = "webauthn"; +} diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index 742e69758b..74173a7e9d 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -1,10 +1,8 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.Security.Claims; +using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.Services; @@ -42,8 +40,22 @@ public class ProfileService : IProfileService public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var existingClaims = context.Subject.Claims; - var newClaims = new List(); + // If the client is a Send client, we do not add any additional claims + if (context.Client.ClientId == BitwardenClient.Send) + { + // preserve all claims that were already on context.Subject + // which includes the ones added by the SendAccessGrantValidator + context.IssuedClaims.AddRange(existingClaims); + return; + } + + // Whenever IdentityServer issues a new access token or services a UserInfo request, it calls + // GetProfileDataAsync to determine which claims to include in the token or response. + // In normal user identity scenarios, we have to look up the user to get their claims and update + // the issued claims collection as claim info can have changed since the last time the user logged in or the + // last time the token was issued. + var newClaims = new List(); var user = await _userService.GetUserByPrincipalAsync(context.Subject); if (user != null) { @@ -63,12 +75,16 @@ public class ProfileService : IProfileService // filter out any of the new claims var existingClaimsToKeep = existingClaims - .Where(c => !c.Type.StartsWith("org") && - (newClaims.Count == 0 || !newClaims.Any(nc => nc.Type == c.Type))) - .ToList(); + .Where(c => + // Drop any org claims + !c.Type.StartsWith("org") && + // If we have no new claims, then keep the existing claims + // If we have new claims, then keep the existing claim if it does not match a new claim type + (newClaims.Count == 0 || !newClaims.Any(nc => nc.Type == c.Type)) + ).ToList(); newClaims.AddRange(existingClaimsToKeep); - if (newClaims.Any()) + if (newClaims.Count != 0) { context.IssuedClaims.AddRange(newClaims); } @@ -76,6 +92,13 @@ public class ProfileService : IProfileService public async Task IsActiveAsync(IsActiveContext context) { + // Send Tokens are not refreshed so when the token has expired the user must request a new one via the authentication method assigned to the send. + if (context.Client.ClientId == BitwardenClient.Send) + { + context.IsActive = true; + return; + } + // We add the security stamp claim to the persisted grant when we issue the refresh token. // IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that // was persisted matches the current security stamp of the user. If it does not match, then the user has performed diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs new file mode 100644 index 0000000000..343c15bd30 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs @@ -0,0 +1,11 @@ +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; + +/// +/// These control the results of the SendGrantValidator. +/// +internal enum SendGrantValidatorResultTypes +{ + ValidSendGuid, + MissingSendId, + InvalidSendId +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs new file mode 100644 index 0000000000..1950ca2978 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs @@ -0,0 +1,9 @@ +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; + +/// +/// These control the results of the SendPasswordValidator. +/// +internal enum SendPasswordValidatorResultTypes +{ + RequestPasswordDoesNotMatch +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs new file mode 100644 index 0000000000..a6f33175bd --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs @@ -0,0 +1,16 @@ +using Bit.Core.Tools.Models.Data; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +public interface ISendPasswordRequestValidator +{ + /// + /// Validates the send password hash against the client hashed password. + /// If this method fails then it will automatically set the context.Result to an invalid grant result. + /// + /// request context + /// resource password authentication method containing the hash of the Send being retrieved + /// returns the result of the validation; A failed result will be an error a successful will contain the claims and a success + GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId); +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs new file mode 100644 index 0000000000..020b3ec5d4 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -0,0 +1,150 @@ +using System.Security.Claims; +using Bit.Core; +using Bit.Core.Identity; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +public class SendAccessGrantValidator( + ISendAuthenticationQuery _sendAuthenticationQuery, + ISendPasswordRequestValidator _sendPasswordRequestValidator, + IFeatureService _featureService) +: IExtensionGrantValidator +{ + string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess; + + private static readonly Dictionary + _sendGrantValidatorErrors = new() + { + { SendGrantValidatorResultTypes.MissingSendId, "send_id is required." }, + { SendGrantValidatorResultTypes.InvalidSendId, "send_id is invalid." } + }; + + + public async Task ValidateAsync(ExtensionGrantValidationContext context) + { + // Check the feature flag + if (!_featureService.IsEnabled(FeatureFlagKeys.SendAccess)) + { + context.Result = new GrantValidationResult(TokenRequestErrors.UnsupportedGrantType); + return; + } + + var (sendIdGuid, result) = GetRequestSendId(context); + if (result != SendGrantValidatorResultTypes.ValidSendGuid) + { + context.Result = BuildErrorResult(result); + return; + } + + // Look up send by id + var method = await _sendAuthenticationQuery.GetAuthenticationMethod(sendIdGuid); + + switch (method) + { + case NeverAuthenticate: + // null send scenario. + // TODO PM-22675: Add send enumeration protection here (primarily benefits self hosted instances). + // We should only map to password or email + OTP protected. + // If user submits password guess for a falsely protected send, then we will return invalid password. + // If user submits email + OTP guess for a falsely protected send, then we will return email sent, do not actually send an email. + context.Result = BuildErrorResult(SendGrantValidatorResultTypes.InvalidSendId); + return; + + case NotAuthenticated: + // automatically issue access token + context.Result = BuildBaseSuccessResult(sendIdGuid); + return; + + case ResourcePassword rp: + // TODO PM-22675: Validate if the password is correct. + context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid); + return; + case EmailOtp eo: + // TODO PM-22678: We will either send the OTP here or validate it based on if otp exists in the request. + // SendOtpToEmail(eo.Emails) or ValidateOtp(eo.Emails); + // break; + + default: + // shouldn’t ever hit this + throw new InvalidOperationException($"Unknown auth method: {method.GetType()}"); + } + } + + /// + /// tries to parse the send_id from the request. + /// If it is not present or invalid, sets the correct result error. + /// + /// request context + /// a parsed sendId Guid and success result or a Guid.Empty and error type otherwise + private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionGrantValidationContext context) + { + var request = context.Request.Raw; + var sendId = request.Get("send_id"); + + // if the sendId is null then the request is the wrong shape and the request is invalid + if (sendId == null) + { + return (Guid.Empty, SendGrantValidatorResultTypes.MissingSendId); + } + // the send_id is not null so the request is the correct shape, so we will attempt to parse it + try + { + var guidBytes = CoreHelpers.Base64UrlDecode(sendId); + var sendGuid = new Guid(guidBytes); + // Guid.Empty indicates an invalid send_id return invalid grant + if (sendGuid == Guid.Empty) + { + return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId); + } + return (sendGuid, SendGrantValidatorResultTypes.ValidSendGuid); + } + catch + { + return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId); + } + } + + /// + /// Builds an error result for the specified error type. + /// + /// The error type. + /// The error result. + private static GrantValidationResult BuildErrorResult(SendGrantValidatorResultTypes error) + { + return error switch + { + // Request is the wrong shape + SendGrantValidatorResultTypes.MissingSendId => new GrantValidationResult( + TokenRequestErrors.InvalidRequest, + errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.MissingSendId]), + // Request is correct shape but data is bad + SendGrantValidatorResultTypes.InvalidSendId => new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.InvalidSendId]), + // should never get here + _ => new GrantValidationResult(TokenRequestErrors.InvalidRequest) + }; + } + + private static GrantValidationResult BuildBaseSuccessResult(Guid sendId) + { + var claims = new List + { + new(Claims.SendId, sendId.ToString()), + new(Claims.Type, IdentityClientType.Send.ToString()) + }; + + return new GrantValidationResult( + subject: sendId.ToString(), + authenticationMethod: CustomGrantTypes.SendAccess, + claims: claims); + } +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs new file mode 100644 index 0000000000..194a0aaa5c --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs @@ -0,0 +1,67 @@ +using System.Security.Claims; +using Bit.Core.Identity; +using Bit.Core.KeyManagement.Sends; +using Bit.Core.Tools.Models.Data; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendPasswordRequestValidator +{ + private readonly ISendPasswordHasher _sendPasswordHasher = sendPasswordHasher; + + /// + /// static object that contains the error messages for the SendPasswordRequestValidator. + /// + private static Dictionary _sendPasswordValidatorErrors = new() + { + { SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch, "Request Password hash is invalid." } + }; + + public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId) + { + var request = context.Request.Raw; + var clientHashedPassword = request.Get("password_hash"); + + if (string.IsNullOrEmpty(clientHashedPassword)) + { + return new GrantValidationResult( + TokenRequestErrors.InvalidRequest, + errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]); + } + + var hashMatches = _sendPasswordHasher.PasswordHashMatches( + resourcePassword.Hash, clientHashedPassword); + + if (!hashMatches) + { + return new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]); + } + + return BuildSendPasswordSuccessResult(sendId); + } + + /// + /// Builds a successful validation result for the Send password send_access grant. + /// + /// + /// + private static GrantValidationResult BuildSendPasswordSuccessResult(Guid sendId) + { + var claims = new List + { + new(Claims.SendId, sendId.ToString()), + new(Claims.Type, IdentityClientType.Send.ToString()) + }; + + return new GrantValidationResult( + subject: sendId.ToString(), + authenticationMethod: CustomGrantTypes.SendAccess, + claims: claims); + } +} diff --git a/src/Identity/IdentityServer/StaticClientStore.cs b/src/Identity/IdentityServer/StaticClientStore.cs index e6880b7670..cab7844f47 100644 --- a/src/Identity/IdentityServer/StaticClientStore.cs +++ b/src/Identity/IdentityServer/StaticClientStore.cs @@ -1,6 +1,7 @@ using System.Collections.Frozen; using Bit.Core.Enums; using Bit.Core.Settings; +using Bit.Identity.IdentityServer.StaticClients; using Duende.IdentityServer.Models; namespace Bit.Identity.IdentityServer; @@ -9,16 +10,17 @@ public class StaticClientStore { public StaticClientStore(GlobalSettings globalSettings) { - ApiClients = new List + Clients = new List { new ApiClient(globalSettings, BitwardenClient.Mobile, 60, 1), new ApiClient(globalSettings, BitwardenClient.Web, 7, 1), new ApiClient(globalSettings, BitwardenClient.Browser, 30, 1), new ApiClient(globalSettings, BitwardenClient.Desktop, 30, 1), new ApiClient(globalSettings, BitwardenClient.Cli, 30, 1), - new ApiClient(globalSettings, BitwardenClient.DirectoryConnector, 30, 24) + new ApiClient(globalSettings, BitwardenClient.DirectoryConnector, 30, 24), + SendClientBuilder.Build(globalSettings), }.ToFrozenDictionary(c => c.ClientId); } - public FrozenDictionary ApiClients { get; } + public FrozenDictionary Clients { get; } } diff --git a/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs b/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs new file mode 100644 index 0000000000..7197d435ed --- /dev/null +++ b/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs @@ -0,0 +1,31 @@ +using Bit.Core.Enums; +using Bit.Core.IdentityServer; +using Bit.Core.Settings; +using Bit.Identity.IdentityServer.Enums; +using Duende.IdentityServer.Models; + +namespace Bit.Identity.IdentityServer.StaticClients; +public static class SendClientBuilder +{ + public static Client Build(GlobalSettings globalSettings) + { + return new Client() + { + ClientId = BitwardenClient.Send, + AllowedGrantTypes = [CustomGrantTypes.SendAccess], + AccessTokenLifetime = 60 * globalSettings.SendAccessTokenLifetimeInMinutes, + + // Do not allow refresh tokens to be issued. + AllowOfflineAccess = false, + + // Send is a public anonymous client, so no secret is required (or really possible to use securely). + RequireClientSecret = false, + + // Allow web vault to use this client. + AllowedCorsOrigins = [globalSettings.BaseServiceUri.Vault], + + // Setup API scopes that the client can request in the scope property of the token request. + AllowedScopes = [ApiScopes.ApiSendAccess], + }; + } +} diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 1476a5ec76..d4f2ad8045 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Bit.Core.Utilities; using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.ClientProviders; using Bit.Identity.IdentityServer.RequestValidators; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.SharedWeb.Utilities; using Duende.IdentityServer.ResponseHandling; using Duende.IdentityServer.Services; @@ -25,6 +26,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services @@ -55,7 +57,8 @@ public static class ServiceCollectionExtensions .AddResourceOwnerValidator() .AddClientStore() .AddIdentityServerCertificate(env, globalSettings) - .AddExtensionGrantValidator(); + .AddExtensionGrantValidator() + .AddExtensionGrantValidator(); if (!globalSettings.SelfHosted) { diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs new file mode 100644 index 0000000000..f27da6e02e --- /dev/null +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs @@ -0,0 +1,271 @@ +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.IdentityServer; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.IntegrationTestCommon.Factories; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.IntegrationTest.RequestValidation; + +// in order to test the default case for the authentication method, we need to create a custom one so we can ensure the +// method throws as expected. +internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { } + +public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory factory) : IClassFixture +{ + private readonly IdentityApplicationFactory _factory = factory; + + [Fact] + public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType() + { + // Arrange + var sendId = Guid.NewGuid(); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Mock feature service to return false + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(false); + services.AddSingleton(featureService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("unsupported_grant_type", content); + } + + [Fact] + public async Task SendAccessGrant_ValidNotAuthenticatedSend_ReturnsAccessToken() + { + // Arrange + var sendId = Guid.NewGuid(); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Mock feature service to return true + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + + // Mock send authentication query + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new NotAuthenticated()); + services.AddSingleton(sendAuthQuery); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("access_token", content); + Assert.Contains("bearer", content.ToLower()); + } + + [Fact] + public async Task SendAccessGrant_MissingSendId_ReturnsInvalidRequest() + { + // Arrange + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + }); + }).CreateClient(); + + var requestBody = new FormUrlEncodedContent([ + new KeyValuePair("grant_type", CustomGrantTypes.SendAccess), + new KeyValuePair("client_id", BitwardenClient.Send) + ]); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("invalid_request", content); + Assert.Contains("send_id is required", content); + } + + [Fact] + public async Task SendAccessGrant_EmptySendGuid_ReturnsInvalidGrant() + { + // Arrange + var sendId = Guid.Empty; + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("invalid_grant", content); + } + + [Fact] + public async Task SendAccessGrant_NeverAuthenticateSend_ReturnsInvalidGrant() + { + // Arrange + var sendId = Guid.NewGuid(); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new NeverAuthenticate()); + services.AddSingleton(sendAuthQuery); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("invalid_grant", content); + } + + [Fact] + public async Task SendAccessGrant_UnknownAuthenticationMethod_ThrowsInvalidOperation() + { + // Arrange + var sendId = Guid.NewGuid(); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId).Returns(new AnUnknownAuthenticationMethod()); + services.AddSingleton(sendAuthQuery); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); + + // Act + var error = await client.PostAsync("/connect/token", requestBody); + + // Assert + // We want to parse the response and ensure we get the correct error from the server + var content = await error.Content.ReadAsStringAsync(); + Assert.Contains("invalid_grant", content); + } + + [Fact] + public async Task SendAccessGrant_PasswordProtectedSend_CallsPasswordValidator() + { + // Arrange + var sendId = Guid.NewGuid(); + var resourcePassword = new ResourcePassword("test-password-hash"); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.SendAccess).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId).Returns(resourcePassword); + services.AddSingleton(sendAuthQuery); + + // Mock password validator to return success + var passwordValidator = Substitute.For(); + passwordValidator.ValidateSendPassword( + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(new GrantValidationResult( + subject: sendId.ToString(), + authenticationMethod: CustomGrantTypes.SendAccess)); + services.AddSingleton(passwordValidator); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, "password123"); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("access_token", content); + Assert.Contains("Bearer", content); + } + + private static FormUrlEncodedContent CreateTokenRequestBody( + Guid sendId, + string password = null, + string sendEmail = null, + string emailOtp = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + var parameters = new List> + { + new("grant_type", CustomGrantTypes.SendAccess), + new("client_id", BitwardenClient.Send ), + new("scope", ApiScopes.ApiSendAccess), + new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()), + new("send_id", sendIdBase64) + }; + + if (!string.IsNullOrEmpty(password)) + { + parameters.Add(new("password_hash", password)); + } + + if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail)) + { + parameters.AddRange( + [ + new KeyValuePair("email", sendEmail), + new KeyValuePair("email_otp", emailOtp) + ]); + } + + return new FormUrlEncodedContent(parameters); + } +} diff --git a/test/Identity.IntegrationTest/openid-configuration.json b/test/Identity.IntegrationTest/openid-configuration.json index 4d74f66009..96014764bd 100644 --- a/test/Identity.IntegrationTest/openid-configuration.json +++ b/test/Identity.IntegrationTest/openid-configuration.json @@ -15,6 +15,7 @@ "api.installation", "internal", "api.secrets", + "api.send.access", "offline_access" ], "claims_supported": [ @@ -33,6 +34,7 @@ "providerserviceuser", "accesssecretsmanager", "sub", + "send_id", "organization" ], "grant_types_supported": [ @@ -43,7 +45,8 @@ "password", "urn:ietf:params:oauth:grant-type:device_code", "urn:openid:params:grant-type:ciba", - "webauthn" + "webauthn", + "send_access" ], "response_types_supported": [ "code", diff --git a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs new file mode 100644 index 0000000000..94f4c1d224 --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs @@ -0,0 +1,333 @@ +using System.Collections.Specialized; +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.IdentityServer; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Extensions; +using Duende.IdentityServer.Validation; +using IdentityModel; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer; + +[SutProviderCustomize] +public class SendAccessGrantValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateAsync_FeatureFlagDisabled_ReturnsUnsupportedGrantType( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.SendAccess) + .Returns(false); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.True(context.Result.IsError); + Assert.Equal(OidcConstants.TokenErrors.UnsupportedGrantType, context.Result.Error); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_MissingSendId_ReturnsInvalidRequest( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.SendAccess) + .Returns(true); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, context.Result.Error); + Assert.Equal("send_id is required.", context.Result.ErrorDescription); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_InvalidSendId_ReturnsInvalidGrant( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.SendAccess) + .Returns(true); + + var context = new ExtensionGrantValidationContext(); + + tokenRequest.GrantType = CustomGrantTypes.SendAccess; + tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty); + + // To preserve the CreateTokenRequestBody method for more general usage we over write the sendId + tokenRequest.Raw.Set("send_id", "invalid-guid-format"); + context.Request = tokenRequest; + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); + Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_EmptyGuidSendId_ReturnsInvalidGrant( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + Guid.Empty, // Empty Guid as sendId + tokenRequest); + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); + Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider, + Guid sendId) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + sendId, + tokenRequest); + + sutProvider.GetDependency() + .GetAuthenticationMethod(sendId) + .Returns(new NeverAuthenticate()); + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); + Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_NotAuthenticatedMethod_ReturnsSuccess( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider, + Guid sendId) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + sendId, + tokenRequest); + + sutProvider.GetDependency() + .GetAuthenticationMethod(sendId) + .Returns(new NotAuthenticated()); + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.False(context.Result.IsError); + // get the claims principal from the result + var subject = context.Result.Subject; + Assert.NotNull(subject); + Assert.Equal(sendId.ToString(), subject.GetSubjectId()); + Assert.Equal(CustomGrantTypes.SendAccess, subject.GetAuthenticationMethod()); + // get the claims from the subject + var claims = subject.Claims.ToList(); + Assert.NotEmpty(claims); + Assert.Contains(claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString()); + Assert.Contains(claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_ResourcePasswordMethod_CallsPasswordValidator( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider, + Guid sendId, + ResourcePassword resourcePassword, + GrantValidationResult expectedResult) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + sendId, + tokenRequest); + + sutProvider.GetDependency() + .GetAuthenticationMethod(sendId) + .Returns(resourcePassword); + + sutProvider.GetDependency() + .ValidateSendPassword(context, resourcePassword, sendId) + .Returns(expectedResult); + + // Act + await sutProvider.Sut.ValidateAsync(context); + + // Assert + Assert.Equal(expectedResult, context.Result); + sutProvider.GetDependency() + .Received(1) + .ValidateSendPassword(context, resourcePassword, sendId); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_EmailOtpMethod_NotImplemented_ThrowsError( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider, + Guid sendId, + EmailOtp emailOtp) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + sendId, + tokenRequest); + + + sutProvider.GetDependency() + .GetAuthenticationMethod(sendId) + .Returns(emailOtp); + + // Act + // Assert + // Currently the EmailOtp case doesn't set a result, so it should be null + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(context)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_UnknownAuthMethod_ThrowsInvalidOperationException( + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + SutProvider sutProvider, + Guid sendId) + { + // Arrange + var context = SetupTokenRequest( + sutProvider, + sendId, + tokenRequest); + + // Create a mock authentication method that's not handled + var unknownMethod = Substitute.For(); + sutProvider.GetDependency() + .GetAuthenticationMethod(sendId) + .Returns(unknownMethod); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ValidateAsync(context)); + + Assert.StartsWith("Unknown auth method:", exception.Message); + } + + [Fact] + public void GrantType_ReturnsCorrectType() + { + // Arrange & Act + var validator = new SendAccessGrantValidator(null!, null!, null!); + + // Assert + Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType); + } + + /// + /// Mutator method fo the SutProvider and the Context to set up a valid request + /// + /// current sut provider + /// test context + /// the send id + /// the token request + private static ExtensionGrantValidationContext SetupTokenRequest( + SutProvider sutProvider, + Guid sendId, + ValidatedTokenRequest request) + { + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.SendAccess) + .Returns(true); + + var context = new ExtensionGrantValidationContext(); + + request.GrantType = CustomGrantTypes.SendAccess; + request.Raw = CreateTokenRequestBody(sendId); + context.Request = request; + + return context; + } + + private static NameValueCollection CreateTokenRequestBody( + Guid sendId, + string passwordHash = null, + string sendEmail = null, + string otpCode = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + + var rawRequestParameters = new NameValueCollection + { + { "grant_type", CustomGrantTypes.SendAccess }, + { "client_id", BitwardenClient.Send }, + { "scope", ApiScopes.ApiSendAccess }, + { "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() }, + { "send_id", sendIdBase64 } + }; + + if (passwordHash != null) + { + rawRequestParameters.Add("password_hash", passwordHash); + } + + if (sendEmail != null) + { + rawRequestParameters.Add("send_email", sendEmail); + } + + if (otpCode != null && sendEmail != null) + { + rawRequestParameters.Add("otp_code", otpCode); + } + + return rawRequestParameters; + } + + // we need a list of sendAuthentication methods to test against since we cannot create new objects in the BitAutoData + public static Dictionary SendAuthenticationMethods => new() + { + { "NeverAuthenticate", new NeverAuthenticate() }, // Send doesn't exist or is deleted + { "NotAuthenticated", new NotAuthenticated() }, // Public send, no auth needed + // TODO: PM-22675 - {"ResourcePassword", new ResourcePassword("clientHashedPassword")}; // Password protected send + // TODO: PM-22678 - {"EmailOtp", new EmailOtp(["emailOtp@test.dev"]}; // Email + OTP protected send + }; +} From 4e6a036f222d8cf9f4ea80053e120a7073ba8b05 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Thu, 14 Aug 2025 09:30:12 -0400 Subject: [PATCH 138/326] Temporarily hold sarif uploads (#6166) --- .github/workflows/build.yml | 12 ++++++------ .github/workflows/scan.yml | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c170f1188..54c31ee6ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,12 +275,12 @@ jobs: fail-build: false output-format: sarif - - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 - with: - sarif_file: ${{ steps.container-scan.outputs.sarif }} - sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} - ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} +# - name: Upload Grype results to GitHub +# uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 +# with: +# sarif_file: ${{ steps.container-scan.outputs.sarif }} +# sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} +# ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index f1d9370c29..04629ec899 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -38,6 +38,8 @@ jobs: pull-requests: write security-events: write id-token: write + with: + upload-sarif: false quality: name: Sonar From c30c0c1d2aa6b7f7c56a1d96b8a196da3cde4b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:02:00 +0100 Subject: [PATCH 139/326] [PM-12492] Create ResendOrganizationInviteCommand (#6182) * Add IResendOrganizationInviteCommand and ResendOrganizationInviteCommand implementation * Add unit tests for ResendOrganizationInviteCommand to validate invite resend functionality * Refactor Organizations, OrganizationUsers, and Members controllers to use IResendInviteCommand for invite resending functionality * Fix Organizations, OrganizationUsers, and Members controllers to replace IResendInviteCommand with IResendOrganizationInviteCommand * Remove ResendInviteAsync method from IOrganizationService and its implementation in OrganizationService to streamline invite management functionality. * Add IResendOrganizationInviteCommand registration in OrganizationServiceCollectionExtensions --- .../Controllers/OrganizationsController.cs | 11 +- .../OrganizationUsersController.cs | 8 +- .../Public/Controllers/MembersController.cs | 8 +- .../IResendOrganizationInviteCommand.cs | 14 ++ .../ResendOrganizationInviteCommand.cs | 56 +++++++ .../Services/IOrganizationService.cs | 1 - .../Implementations/OrganizationService.cs | 15 -- ...OrganizationServiceCollectionExtensions.cs | 1 + .../ResendOrganizationInviteCommandTests.cs | 137 ++++++++++++++++++ 9 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IResendOrganizationInviteCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommandTests.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 4bbb5db3f0..2417bf610d 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -9,6 +9,7 @@ using Bit.Admin.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; @@ -32,7 +33,6 @@ namespace Bit.Admin.AdminConsole.Controllers; [Authorize] public class OrganizationsController : Controller { - private readonly IOrganizationService _organizationService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationConnectionRepository _organizationConnectionRepository; @@ -55,9 +55,9 @@ public class OrganizationsController : Controller private readonly IProviderBillingService _providerBillingService; private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand; private readonly IPricingClient _pricingClient; + private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; public OrganizationsController( - IOrganizationService organizationService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationConnectionRepository organizationConnectionRepository, @@ -79,9 +79,9 @@ public class OrganizationsController : Controller IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, IProviderBillingService providerBillingService, IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand, - IPricingClient pricingClient) + IPricingClient pricingClient, + IResendOrganizationInviteCommand resendOrganizationInviteCommand) { - _organizationService = organizationService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _organizationConnectionRepository = organizationConnectionRepository; @@ -104,6 +104,7 @@ public class OrganizationsController : Controller _providerBillingService = providerBillingService; _organizationInitiateDeleteCommand = organizationInitiateDeleteCommand; _pricingClient = pricingClient; + _resendOrganizationInviteCommand = resendOrganizationInviteCommand; } [RequirePermission(Permission.Org_List_View)] @@ -395,7 +396,7 @@ public class OrganizationsController : Controller var organizationUsers = await _organizationUserRepository.GetManyByOrganizationAsync(id, OrganizationUserType.Owner); foreach (var organizationUser in organizationUsers) { - await _organizationService.ResendInviteAsync(id, null, organizationUser.Id, true); + await _resendOrganizationInviteCommand.ResendInviteAsync(id, null, organizationUser.Id, true); } return Json(null); diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index bf49f144ce..2b464c24e2 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -12,6 +12,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -62,6 +63,7 @@ public class OrganizationUsersController : Controller private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; private readonly IPricingClient _pricingClient; + private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; @@ -92,7 +94,8 @@ public class OrganizationUsersController : Controller IConfirmOrganizationUserCommand confirmOrganizationUserCommand, IRestoreOrganizationUserCommand restoreOrganizationUserCommand, IInitPendingOrganizationCommand initPendingOrganizationCommand, - IRevokeOrganizationUserCommand revokeOrganizationUserCommand) + IRevokeOrganizationUserCommand revokeOrganizationUserCommand, + IResendOrganizationInviteCommand resendOrganizationInviteCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -116,6 +119,7 @@ public class OrganizationUsersController : Controller _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; _pricingClient = pricingClient; + _resendOrganizationInviteCommand = resendOrganizationInviteCommand; _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; _initPendingOrganizationCommand = initPendingOrganizationCommand; @@ -266,7 +270,7 @@ public class OrganizationUsersController : Controller public async Task Reinvite(Guid orgId, Guid id) { var userId = _userService.GetProperUserId(User); - await _organizationService.ResendInviteAsync(orgId, userId.Value, id); + await _resendOrganizationInviteCommand.ResendInviteAsync(orgId, userId.Value, id); } [HttpPost("{organizationUserId}/accept-init")] diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 6f41016dcd..7bfe5648b6 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -6,6 +6,7 @@ using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; @@ -32,6 +33,7 @@ public class MembersController : Controller private readonly IOrganizationRepository _organizationRepository; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; + private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand; public MembersController( IOrganizationUserRepository organizationUserRepository, @@ -45,7 +47,8 @@ public class MembersController : Controller IPaymentService paymentService, IOrganizationRepository organizationRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IRemoveOrganizationUserCommand removeOrganizationUserCommand) + IRemoveOrganizationUserCommand removeOrganizationUserCommand, + IResendOrganizationInviteCommand resendOrganizationInviteCommand) { _organizationUserRepository = organizationUserRepository; _groupRepository = groupRepository; @@ -59,6 +62,7 @@ public class MembersController : Controller _organizationRepository = organizationRepository; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; + _resendOrganizationInviteCommand = resendOrganizationInviteCommand; } /// @@ -260,7 +264,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id); + await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id); return new OkResult(); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IResendOrganizationInviteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IResendOrganizationInviteCommand.cs new file mode 100644 index 0000000000..645cdb42d2 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IResendOrganizationInviteCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; + +public interface IResendOrganizationInviteCommand +{ + /// + /// Resend an invite to an organization user. + /// + /// The ID of the organization. + /// The ID of the user who is inviting the organization user. + /// The ID of the organization user to resend the invite to. + /// Whether to initialize the organization. + /// This is should only be true when inviting the owner of a new organization. + Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs new file mode 100644 index 0000000000..7e68af7816 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommand.cs @@ -0,0 +1,56 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.DebuggingInstruments; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; + +public class ResendOrganizationInviteCommand : IResendOrganizationInviteCommand +{ + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; + private readonly ILogger _logger; + + public ResendOrganizationInviteCommand( + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, + ILogger logger) + { + _organizationUserRepository = organizationUserRepository; + _organizationRepository = organizationRepository; + _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; + _logger = logger; + } + + public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, + bool initOrganization = false) + { + var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + if (organizationUser == null || organizationUser.OrganizationId != organizationId || + organizationUser.Status != OrganizationUserStatusType.Invited) + { + throw new BadRequestException("User invalid."); + } + + _logger.LogUserInviteStateDiagnostics(organizationUser); + + var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId); + if (organization == null) + { + throw new BadRequestException("Organization invalid."); + } + await SendInviteAsync(organizationUser, organization, initOrganization); + } + + private async Task SendInviteAsync(OrganizationUser organizationUser, Organization organization, bool initOrganization) => + await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest( + users: [organizationUser], + organization: organization, + initOrganization: initOrganization)); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 05c84c731c..6adfc4772f 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -29,7 +29,6 @@ public interface IOrganizationService Task> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites); Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); - Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); Task ImportAsync(Guid organizationId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index b1a07338a3..41e4f2f618 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -766,21 +766,6 @@ public class OrganizationService : IOrganizationService return result; } - public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, - bool initOrganization = false) - { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null || orgUser.OrganizationId != organizationId || - orgUser.Status != OrganizationUserStatusType.Invited) - { - throw new BadRequestException("User invalid."); - } - - _logger.LogUserInviteStateDiagnostics(orgUser); - - var org = await GetOrgById(orgUser.OrganizationId); - await SendInviteAsync(orgUser, org, initOrganization); - } private async Task SendInvitesAsync(IEnumerable orgUsers, Organization organization) => await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization)); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 234d6f1a84..bcbaccca7c 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -186,6 +186,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommandTests.cs new file mode 100644 index 0000000000..7fad49af24 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/ResendOrganizationInviteCommandTests.cs @@ -0,0 +1,137 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; + +[SutProviderCustomize] +public class ResendOrganizationInviteCommandTests +{ + [Theory] + [BitAutoData] + public async Task ResendInviteAsync_WhenValidUserAndOrganization_SendsInvite( + Organization organization, + OrganizationUser organizationUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = organization.Id; + organizationUser.Status = OrganizationUserStatusType.Invited; + + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + // Act + await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .SendInvitesAsync(Arg.Is(req => + req.Organization == organization && + req.Users.Length == 1 && + req.Users[0] == organizationUser && + req.InitOrganization == false)); + } + + [Theory] + [BitAutoData] + public async Task ResendInviteAsync_WhenInitOrganizationTrue_SendsInviteWithInitFlag( + Organization organization, + OrganizationUser organizationUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = organization.Id; + organizationUser.Status = OrganizationUserStatusType.Invited; + + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + // Act + await sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id, initOrganization: true); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .SendInvitesAsync(Arg.Is(req => + req.Organization == organization && + req.Users.Length == 1 && + req.Users[0] == organizationUser && + req.InitOrganization == true)); + } + + [Theory] + [BitAutoData] + public async Task ResendInviteAsync_WhenOrganizationUserInvalid_ThrowsBadRequest( + Organization organization, + OrganizationUser organizationUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = organization.Id; + organizationUser.Status = OrganizationUserStatusType.Accepted; + + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + // Act + Assert + var ex = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id)); + + Assert.Equal("User invalid.", ex.Message); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendInvitesAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task ResendInviteAsync_WhenOrganizationNotFound_ThrowsBadRequest( + Organization organization, + OrganizationUser organizationUser, + SutProvider sutProvider) + { + // Arrange + organizationUser.OrganizationId = organization.Id; + organizationUser.Status = OrganizationUserStatusType.Invited; + + sutProvider.GetDependency() + .GetByIdAsync(organizationUser.Id) + .Returns(organizationUser); + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns((Organization?)null); + + // Act + Assert + var ex = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendInviteAsync(organization.Id, invitingUserId: null, organizationUser.Id)); + + Assert.Equal("Organization invalid.", ex.Message); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendInvitesAsync(Arg.Any()); + } +} From 4b751e8cbf128da1fefab0204de0b6940cf64af0 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:43:30 +0200 Subject: [PATCH 140/326] Add feature flag for chromium importer feature (#6193) 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 1ba80d3c9b..1af5037792 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -203,6 +203,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 UseChromiumImporter = "pm-23982-chromium-importer"; /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; From 4bad008085f6ab6d3f850404f182c256d8d07834 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:08:41 -0400 Subject: [PATCH 141/326] chore(comments): [PM-24624] Add more comments to AutoProvisionUserAsync * Comments in auto-provisioning logic. * More clarifications. * Changed method name. * Updated response from method. * Clarified message. --- .../src/Sso/Controllers/AccountController.cs | 166 ++++++++++++------ 1 file changed, 112 insertions(+), 54 deletions(-) diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 5776912bd3..7fadc8cb27 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -3,6 +3,7 @@ using System.Security.Claims; using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -254,25 +255,25 @@ public class AccountController : Controller var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}"); _logger.LogDebug("External claims: {@claims}", externalClaims); - // Lookup our user and external provider info - // Note: the user will only exist if the user has already been provisioned and exists in the User table and the SSO user table. + // See if the user has logged in with this SSO provider before and has already been provisioned. + // This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using. var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result); + + // The user has not authenticated with this SSO provider before. + // They could have an existing Bitwarden account in the User table though. if (user == null) { - // User does not exist in SSO User table. They could have an existing BW account in the User table. - - // This might be where you might initiate a custom workflow for user registration - // in this sample we don't show how that would be done, as our sample implementation - // simply auto-provisions new external user + // If we're manually linking to SSO, the user's external identifier will be passed as query string parameter. var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ? result.Properties.Items["user_identifier"] : null; user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData); } + // Either the user already authenticated with the SSO provider, or we've just provisioned them. + // Either way, we have associated the SSO login with a Bitwarden user. + // We will now sign the Bitwarden user in. if (user != null) { - // User was JIT provisioned (this could be an existing user or a new user) - // This allows us to collect any additional claims or properties // for the specific protocols used and store them in the local auth cookie. // this is typically used to store data needed for signout from those protocols. @@ -350,6 +351,10 @@ public class AccountController : Controller } } + /// + /// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`. + /// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records. + /// private async Task<(User user, string provider, string providerUserId, IEnumerable claims, SsoConfigurationData config)> FindUserFromExternalProviderAsync(AuthenticateResult result) { @@ -407,6 +412,23 @@ public class AccountController : Controller return (user, provider, providerUserId, claims, ssoConfigData); } + /// + /// Provision an SSO-linked Bitwarden user. + /// This handles three different scenarios: + /// 1. Creating an SsoUser link for an existing User and OrganizationUser + /// - User is a member of the organization, but hasn't authenticated with the org's SSO provider before. + /// 2. Creating a new User and a new OrganizationUser, then establishing an SsoUser link + /// - User is joining the organization through JIT provisioning, without a pending invitation + /// 3. Creating a new User for an existing OrganizationUser (created by invitation), then establishing an SsoUser link + /// - User is signing in with a pending invitation. + /// + /// The external identity provider. + /// The external identity provider's user identifier. + /// The claims from the external IdP. + /// The user identifier used for manual SSO linking. + /// The SSO configuration for the organization. + /// The User to sign in. + /// An exception if the user cannot be provisioned as requested. private async Task AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable claims, string userIdentifier, SsoConfigurationData config) { @@ -434,50 +456,15 @@ public class AccountController : Controller } else { - var split = userIdentifier.Split(","); - if (split.Length < 2) - { - throw new Exception(_i18nService.T("InvalidUserIdentifier")); - } - var userId = split[0]; - var token = split[1]; - - var tokenOptions = new TokenOptions(); - - var claimedUser = await _userService.GetUserByIdAsync(userId); - if (claimedUser != null) - { - var tokenIsValid = await _userManager.VerifyUserTokenAsync( - claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token); - if (tokenIsValid) - { - existingUser = claimedUser; - } - else - { - throw new Exception(_i18nService.T("UserIdAndTokenMismatch")); - } - } + existingUser = await GetUserFromManualLinkingData(userIdentifier); } - OrganizationUser orgUser = null; - var organization = await _organizationRepository.GetByIdAsync(orgId); - if (organization == null) - { - throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId)); - } + // Try to find the OrganizationUser if it exists. + var (organization, orgUser) = await FindOrganizationUser(existingUser, email, orgId); - // Try to find OrgUser via existing User Id (accepted/confirmed user) - if (existingUser != null) - { - var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id); - orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId); - } - - // If no Org User found by Existing User Id - search all organization users via email - orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email); - - // All Existing User flows handled below + //---------------------------------------------------- + // Scenario 1: We've found the user in the User table + //---------------------------------------------------- if (existingUser != null) { if (existingUser.UsesKeyConnector && @@ -486,6 +473,8 @@ public class AccountController : Controller throw new Exception(_i18nService.T("UserAlreadyExistsKeyConnector")); } + // If the user already exists in Bitwarden, we require that the user already be in the org, + // and that they are either Accepted or Confirmed. if (orgUser == null) { // Org User is not created - no invite has been sent @@ -495,7 +484,11 @@ public class AccountController : Controller EnsureOrgUserStatusAllowed(orgUser.Status, organization.DisplayName(), allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]); - // Accepted or Confirmed - create SSO link and return; + + // Since we're in the auto-provisioning logic, this means that the user exists, but they have not + // authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them). + // We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed + // with authentication. await CreateSsoUserRecord(providerUserId, existingUser.Id, orgId, orgUser); return existingUser; } @@ -538,7 +531,9 @@ public class AccountController : Controller emailVerified = organizationDomain?.VerifiedDate.HasValue ?? false; } - // Create user record - all existing user flows are handled above + //-------------------------------------------------- + // Scenarios 2 and 3: We need to register a new user + //-------------------------------------------------- var user = new User { Name = name, @@ -564,7 +559,11 @@ public class AccountController : Controller await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email); } - // Create Org User if null or else update existing Org User + //----------------------------------------------------------------- + // Scenario 2: We also need to create an OrganizationUser + // This means that an invitation was not sent for this user and we + // need to establish their invited status now. + //----------------------------------------------------------------- if (orgUser == null) { orgUser = new OrganizationUser @@ -576,18 +575,77 @@ public class AccountController : Controller }; await _organizationUserRepository.CreateAsync(orgUser); } + //----------------------------------------------------------------- + // Scenario 3: There is already an existing OrganizationUser + // That was established through an invitation. We just need to + // update the UserId now that we have created a User record. + //----------------------------------------------------------------- else { orgUser.UserId = user.Id; await _organizationUserRepository.ReplaceAsync(orgUser); } - // Create sso user record + // Create the SsoUser record to link the user to the SSO provider. await CreateSsoUserRecord(providerUserId, user.Id, orgId, orgUser); return user; } + private async Task GetUserFromManualLinkingData(string userIdentifier) + { + User user = null; + var split = userIdentifier.Split(","); + if (split.Length < 2) + { + throw new Exception(_i18nService.T("InvalidUserIdentifier")); + } + var userId = split[0]; + var token = split[1]; + + var tokenOptions = new TokenOptions(); + + var claimedUser = await _userService.GetUserByIdAsync(userId); + if (claimedUser != null) + { + var tokenIsValid = await _userManager.VerifyUserTokenAsync( + claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token); + if (tokenIsValid) + { + user = claimedUser; + } + else + { + throw new Exception(_i18nService.T("UserIdAndTokenMismatch")); + } + } + return user; + } + + private async Task<(Organization, OrganizationUser)> FindOrganizationUser(User existingUser, string email, Guid orgId) + { + OrganizationUser orgUser = null; + var organization = await _organizationRepository.GetByIdAsync(orgId); + if (organization == null) + { + throw new Exception(_i18nService.T("CouldNotFindOrganization", orgId)); + } + + // Try to find OrgUser via existing User Id. + // This covers any OrganizationUser state after they have accepted an invite. + if (existingUser != null) + { + var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id); + orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId); + } + + // If no Org User found by Existing User Id - search all the organization's users via email. + // This covers users who are Invited but haven't accepted their invite yet. + orgUser ??= await _organizationUserRepository.GetByOrganizationEmailAsync(orgId, email); + + return (organization, orgUser); + } + private void EnsureOrgUserStatusAllowed( OrganizationUserStatusType status, string organizationDisplayName, From 41f82bb3577075016180d927f2a8c1c304290deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:14:38 +0100 Subject: [PATCH 142/326] [PM-23116/PM-23117] Remove deprecated feature flag MembersGetEndpointOptimization (#6179) * Refactor OrganizationUserRepositoryTests: Swap GetManyByOrganizationWithClaimedDomainsAsync_vNext with GetManyByOrganizationWithClaimedDomainsAsync and remove outdated test * Refactor GetOrganizationUsersClaimedStatusQuery: Remove unused IFeatureService dependency and simplify domain claimed status retrieval logic. * Refactor OrganizationUserUserDetailsQuery: Remove unused IFeatureService dependency and streamline user details retrieval methods. * Refactor OrganizationUserRepository: Remove deprecated GetManyByOrganizationWithClaimedDomainsAsync_vNext method and its implementation * Remove deprecated feature flag MembersGetEndpointOptimization --- .../GetOrganizationUsersClaimedStatusQuery.cs | 9 +- .../OrganizationUserUserDetailsQuery.cs | 56 ++------ .../IOrganizationUserRepository.cs | 4 - src/Core/Constants.cs | 1 - .../OrganizationUserRepository.cs | 13 -- .../OrganizationUserRepository.cs | 6 - .../OrganizationUserRepositoryTests.cs | 136 +----------------- 7 files changed, 14 insertions(+), 211 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs index b27da2a22e..d8c510119a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs @@ -8,16 +8,13 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim { private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IFeatureService _featureService; public GetOrganizationUsersClaimedStatusQuery( IApplicationCacheService applicationCacheService, - IOrganizationUserRepository organizationUserRepository, - IFeatureService featureService) + IOrganizationUserRepository organizationUserRepository) { _applicationCacheService = applicationCacheService; _organizationUserRepository = organizationUserRepository; - _featureService = featureService; } public async Task> GetUsersOrganizationClaimedStatusAsync(Guid organizationId, IEnumerable organizationUserIds) @@ -30,9 +27,7 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim if (organizationAbility is { Enabled: true, UseOrganizationDomains: true }) { // Get all organization users with claimed domains by the organization - var organizationUsersWithClaimedDomain = _featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization) - ? await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organizationId) - : await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); + var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); // Create a dictionary with the OrganizationUserId and a boolean indicating if the user is claimed by the organization return organizationUserIds.ToDictionary(ouId => ouId, ouId => organizationUsersWithClaimedDomain.Any(ou => ou.Id == ouId)); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs index aa2cd2df8f..b6152060e8 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs @@ -1,10 +1,8 @@ -using Bit.Core; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Utilities; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; @@ -15,19 +13,16 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer { private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IFeatureService _featureService; private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; public OrganizationUserUserDetailsQuery( IOrganizationUserRepository organizationUserRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IFeatureService featureService, IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery ) { _organizationUserRepository = organizationUserRepository; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _featureService = featureService; _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; } @@ -62,47 +57,6 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer /// Request details for the query /// List of OrganizationUserUserDetails public async Task> Get(OrganizationUserUserDetailsQueryRequest request) - { - if (_featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization)) - { - return await Get_vNext(request); - } - - var organizationUsers = await GetOrganizationUserUserDetails(request); - - var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); - var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); - var responses = organizationUsers.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); - - - return responses; - } - - /// - /// Get the organization users user details, two factor enabled status, and - /// claimed status for confirmed users that are enrolled in account recovery - /// - /// Request details for the query - /// List of OrganizationUserUserDetails - public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) - { - if (_featureService.IsEnabled(FeatureFlagKeys.MembersGetEndpointOptimization)) - { - return await GetAccountRecoveryEnrolledUsers_vNext(request); - } - - var organizationUsers = (await GetOrganizationUserUserDetails(request)) - .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)); - - var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); - var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); - var responses = organizationUsers - .Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); - - return responses; - } - - private async Task> Get_vNext(OrganizationUserUserDetailsQueryRequest request) { var organizationUsers = await _organizationUserRepository .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections); @@ -132,7 +86,13 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return responses; } - private async Task> GetAccountRecoveryEnrolledUsers_vNext(OrganizationUserUserDetailsQueryRequest request) + /// + /// Get the organization users user details, two factor enabled status, and + /// claimed status for confirmed users that are enrolled in account recovery + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) { var organizationUsers = (await _organizationUserRepository .GetManyDetailsByOrganizationAsync_vNext(request.OrganizationId, request.IncludeGroups, request.IncludeCollections)) diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 7187ab50a6..37a830c92e 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -76,10 +76,6 @@ public interface IOrganizationUserRepository : IRepository Task> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId); - /// - /// Optimized version of with better performance. - /// - Task> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId); Task RevokeManyByIdAsync(IEnumerable organizationUserIds); /// diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1af5037792..7cedf42b5b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; - public const string MembersGetEndpointOptimization = "pm-23113-optimize-get-members-endpoint"; public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; /* Auth Team */ diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 8666b5307f..5f389ae56d 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -608,19 +608,6 @@ public class OrganizationUserRepository : Repository, IO } public async Task> GetManyByOrganizationWithClaimedDomainsAsync(Guid organizationId) - { - using (var connection = new SqlConnection(ConnectionString)) - { - var results = await connection.QueryAsync( - $"[{Schema}].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains]", - new { OrganizationId = organizationId }, - commandType: CommandType.StoredProcedure); - - return results.ToList(); - } - } - - public async Task> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 33ffc453d5..c6dd621c28 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -786,12 +786,6 @@ public class OrganizationUserRepository : Repository> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId) - { - // No EF optimization is required for this query - return await GetManyByOrganizationWithClaimedDomainsAsync(organizationId); - } - public async Task RevokeManyByIdAsync(IEnumerable organizationUserIds) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 130add6332..612e8d1074 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -354,134 +354,6 @@ public class OrganizationUserRepositoryTests Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies); } - [DatabaseTheory, DatabaseData] - public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationDomainRepository organizationDomainRepository) - { - var id = Guid.NewGuid(); - var domainName = $"{id}.example.com"; - - var user1 = await userRepository.CreateAsync(new User - { - Name = "Test User 1", - Email = $"test+{id}@{domainName}", - ApiKey = "TEST", - SecurityStamp = "stamp", - Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = 1, - KdfMemory = 2, - KdfParallelism = 3 - }); - - var user2 = await userRepository.CreateAsync(new User - { - Name = "Test User 2", - Email = $"test+{id}@x-{domainName}", // Different domain - ApiKey = "TEST", - SecurityStamp = "stamp", - Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = 1, - KdfMemory = 2, - KdfParallelism = 3 - }); - - var user3 = await userRepository.CreateAsync(new User - { - Name = "Test User 2", - Email = $"test+{id}@{domainName}.example.com", // Different domain - ApiKey = "TEST", - SecurityStamp = "stamp", - Kdf = KdfType.PBKDF2_SHA256, - KdfIterations = 1, - KdfMemory = 2, - KdfParallelism = 3 - }); - - var organization = await organizationRepository.CreateAsync(new Organization - { - Name = $"Test Org {id}", - BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl - Plan = "Test", // TODO: EF does not enforce this being NOT NULl - PrivateKey = "privatekey", - UsePolicies = false, - UseSso = false, - UseKeyConnector = false, - UseScim = false, - UseGroups = false, - UseDirectory = false, - UseEvents = false, - UseTotp = false, - Use2fa = false, - UseApi = false, - UseResetPassword = false, - UseSecretsManager = false, - SelfHost = false, - UsersGetPremium = false, - UseCustomPermissions = false, - Enabled = true, - UsePasswordManager = false, - LimitCollectionCreation = false, - LimitCollectionDeletion = false, - LimitItemDeletion = false, - AllowAdminAccessToAllCollectionItems = false, - UseRiskInsights = false, - UseAdminSponsoredFamilies = false - }); - - var organizationDomain = new OrganizationDomain - { - OrganizationId = organization.Id, - DomainName = domainName, - Txt = "btw+12345", - }; - organizationDomain.SetVerifiedDate(); - organizationDomain.SetNextRunDate(12); - organizationDomain.SetJobRunCount(); - await organizationDomainRepository.CreateAsync(organizationDomain); - - var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user1.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Owner, - ResetPasswordKey = "resetpasswordkey1", - AccessSecretsManager = false - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user2.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - ResetPasswordKey = "resetpasswordkey1", - AccessSecretsManager = false - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user3.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - ResetPasswordKey = "resetpasswordkey1", - AccessSecretsManager = false - }); - - var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); - - Assert.NotNull(responseModel); - Assert.Single(responseModel); - Assert.Equal(orgUser1.Id, responseModel.Single().Id); - } - [DatabaseTheory, DatabaseData] public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository, IUserRepository userRepository, @@ -991,7 +863,7 @@ public class OrganizationUserRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task GetManyByOrganizationWithClaimedDomainsAsync_vNext_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( + public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -1094,7 +966,7 @@ public class OrganizationUserRepositoryTests RevisionDate = requestTime }); - var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organization.Id); + var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); Assert.NotNull(responseModel); Assert.Single(responseModel); @@ -1104,7 +976,7 @@ public class OrganizationUserRepositoryTests } [DatabaseTheory, DatabaseData] - public async Task GetManyByOrganizationWithClaimedDomainsAsync_vNext_WithNoVerifiedDomain_ReturnsEmpty( + public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty( IUserRepository userRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, @@ -1161,7 +1033,7 @@ public class OrganizationUserRepositoryTests RevisionDate = requestTime }); - var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync_vNext(organization.Id); + var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); Assert.NotNull(responseModel); Assert.Empty(responseModel); From 8a36d96e56e0f5f6474cea92148bb5b582e8496e Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 15 Aug 2025 10:06:40 -0400 Subject: [PATCH 143/326] [PM-22739] Add ClaimsPrincipal Extension feat: add ClaimsPrincipal Extension test: add tests --- .../SendAccessClaimsPrincipalExtensions.cs | 22 ++++++++ ...endAccessClaimsPrincipalExtensionsTests.cs | 54 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs create mode 100644 test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs diff --git a/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs new file mode 100644 index 0000000000..1feadaf081 --- /dev/null +++ b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs @@ -0,0 +1,22 @@ +using System.Security.Claims; +using Bit.Core.Identity; + +namespace Bit.Core.Auth.UserFeatures.SendAccess; + +public static class SendAccessClaimsPrincipalExtensions +{ + public static Guid GetSendId(this ClaimsPrincipal user) + { + ArgumentNullException.ThrowIfNull(user); + + var sendIdClaim = user.FindFirst(Claims.SendId) + ?? throw new InvalidOperationException("Send ID claim not found."); + + if (!Guid.TryParse(sendIdClaim.Value, out var sendGuid)) + { + throw new InvalidOperationException("Invalid Send ID claim value."); + } + + return sendGuid; + } +} diff --git a/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs new file mode 100644 index 0000000000..27a0bc1bbc --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs @@ -0,0 +1,54 @@ +using System.Security.Claims; +using Bit.Core.Auth.UserFeatures.SendAccess; +using Bit.Core.Identity; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.SendAccess; + +public class SendAccessClaimsPrincipalExtensionsTests +{ + [Fact] + public void GetSendId_ReturnsGuid_WhenClaimIsPresentAndValid() + { + // Arrange + var guid = Guid.NewGuid(); + var claims = new[] { new Claim(Claims.SendId, guid.ToString()) }; + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + var result = principal.GetSendId(); + + // Assert + Assert.Equal(guid, result); + } + + [Fact] + public void GetSendId_ThrowsInvalidOperationException_WhenClaimIsMissing() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + + // Act & Assert + var ex = Assert.Throws(() => principal.GetSendId()); + Assert.Equal("Send ID claim not found.", ex.Message); + } + + [Fact] + public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid() + { + // Arrange + var claims = new[] { new Claim(Claims.SendId, "not-a-guid") }; + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act & Assert + var ex = Assert.Throws(() => principal.GetSendId()); + Assert.Equal("Invalid Send ID claim value.", ex.Message); + } + + [Fact] + public void GetSendId_ThrowsArgumentNullException_WhenPrincipalIsNull() + { + // Act & Assert + Assert.Throws(() => SendAccessClaimsPrincipalExtensions.GetSendId(null)); + } +} From bd133b936c40167cce78006c857e84fd3e652776 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 18 Aug 2025 09:42:51 -0500 Subject: [PATCH 144/326] [PM-22145] Tax ID notifications for Organizations and Providers (#6185) * Add TaxRegistrationsListAsync to StripeAdapter * Update GetOrganizationWarningsQuery, add GetProviderWarningsQuery to support tax ID warning * Add feature flag to control web display * Run dotnet format' --- .../Queries/GetProviderWarningsQuery.cs | 101 ++++ .../Utilities/ServiceCollectionExtensions.cs | 3 + .../Queries/GetProviderWarningsQueryTests.cs | 523 ++++++++++++++++++ .../BusinessUnitConverterTests.cs | 0 .../ProviderBillingServiceTests.cs | 0 .../ProviderPriceAdapterTests.cs | 0 .../OrganizationBillingController.cs | 27 - .../OrganizationBillingVNextController.cs | 16 +- .../VNext/ProviderBillingVNextController.cs | 11 + .../ManageOrganizationBillingRequirement.cs | 3 +- src/Core/Billing/Constants/StripeConstants.cs | 15 + .../Models/OrganizationWarnings.cs | 6 + .../Queries/GetOrganizationWarningsQuery.cs | 173 +++--- .../Providers/Models/ProviderWarnings.cs | 18 + .../Queries/IGetProviderWarningsQuery.cs | 9 + src/Core/Constants.cs | 1 + src/Core/Services/IStripeAdapter.cs | 1 + .../Services/Implementations/StripeAdapter.cs | 7 + .../GetOrganizationWarningsQueryTests.cs | 12 +- 19 files changed, 821 insertions(+), 105 deletions(-) create mode 100644 bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs rename bitwarden_license/test/Commercial.Core.Test/Billing/Providers/{ => Services}/BusinessUnitConverterTests.cs (100%) rename bitwarden_license/test/Commercial.Core.Test/Billing/Providers/{ => Services}/ProviderBillingServiceTests.cs (100%) rename bitwarden_license/test/Commercial.Core.Test/Billing/Providers/{ => Services}/ProviderPriceAdapterTests.cs (100%) create mode 100644 src/Core/Billing/Providers/Models/ProviderWarnings.cs create mode 100644 src/Core/Billing/Providers/Queries/IGetProviderWarningsQuery.cs diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs new file mode 100644 index 0000000000..9392c285e0 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs @@ -0,0 +1,101 @@ +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Queries; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Stripe; +using Stripe.Tax; + +namespace Bit.Commercial.Core.Billing.Providers.Queries; + +using static StripeConstants; +using SuspensionWarning = ProviderWarnings.SuspensionWarning; +using TaxIdWarning = ProviderWarnings.TaxIdWarning; + +public class GetProviderWarningsQuery( + ICurrentContext currentContext, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : IGetProviderWarningsQuery +{ + public async Task Run(Provider provider) + { + var warnings = new ProviderWarnings(); + + var subscription = + await subscriberService.GetSubscription(provider, + new SubscriptionGetOptions { Expand = ["customer.tax_ids"] }); + + if (subscription == null) + { + return warnings; + } + + warnings.Suspension = GetSuspensionWarning(provider, subscription); + + warnings.TaxId = await GetTaxIdWarningAsync(provider, subscription.Customer); + + return warnings; + } + + private SuspensionWarning? GetSuspensionWarning( + Provider provider, + Subscription subscription) + { + if (provider.Enabled) + { + return null; + } + + return subscription.Status switch + { + SubscriptionStatus.Unpaid => currentContext.ProviderProviderAdmin(provider.Id) + ? new SuspensionWarning { Resolution = "add_payment_method", SubscriptionCancelsAt = subscription.CancelAt } + : new SuspensionWarning { Resolution = "contact_administrator" }, + _ => new SuspensionWarning { Resolution = "contact_support" } + }; + } + + private async Task GetTaxIdWarningAsync( + Provider provider, + Customer customer) + { + if (!currentContext.ProviderProviderAdmin(provider.Id)) + { + return null; + } + + // TODO: Potentially DRY this out with the GetOrganizationWarningsQuery + + // Get active and scheduled registrations + var registrations = (await Task.WhenAll( + stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }), + stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled }))) + .SelectMany(registrations => registrations.Data); + + // Find the matching registration for the customer + var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address.Country); + + // If we're not registered in their country, we don't need a warning + if (registration == null) + { + return null; + } + + var taxId = customer.TaxIds.FirstOrDefault(); + + return taxId switch + { + // Customer's tax ID is missing + null => new TaxIdWarning { Type = "tax_id_missing" }, + // Not sure if this case is valid, but Stripe says this property is nullable + not null when taxId.Verification == null => null, + // Customer's tax ID is pending verification + not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" }, + // Customer's tax ID failed verification + not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" }, + _ => null + }; + } +} diff --git a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs index 34f49e0ccc..022045e64f 100644 --- a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs @@ -1,8 +1,10 @@ using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Services; +using Bit.Commercial.Core.Billing.Providers.Queries; using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Services; +using Bit.Core.Billing.Providers.Queries; using Bit.Core.Billing.Providers.Services; using Microsoft.Extensions.DependencyInjection; @@ -17,5 +19,6 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs new file mode 100644 index 0000000000..f199c44924 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs @@ -0,0 +1,523 @@ +using Bit.Commercial.Core.Billing.Providers.Queries; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Stripe.Tax; +using Xunit; + +namespace Bit.Commercial.Core.Test.Billing.Providers.Queries; + +using static StripeConstants; + +[SutProviderCustomize] +public class GetProviderWarningsQueryTests +{ + private static readonly string[] _requiredExpansions = ["customer.tax_ids"]; + + [Theory, BitAutoData] + public async Task Run_NoSubscription_NoWarnings( + Provider provider, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .ReturnsNull(); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension: null, + TaxId: null + }); + } + + [Theory, BitAutoData] + public async Task Run_ProviderEnabled_NoSuspensionWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Unpaid, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList { Data = [] }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.Null(response!.Suspension); + } + + [Theory, BitAutoData] + public async Task Run_Has_SuspensionWarning_AddPaymentMethod( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = false; + var cancelAt = DateTime.UtcNow.AddDays(7); + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Unpaid, + CancelAt = cancelAt, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList { Data = [] }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension.Resolution: "add_payment_method" + }); + Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt); + } + + [Theory, BitAutoData] + public async Task Run_Has_SuspensionWarning_ContactAdministrator( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Unpaid, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList { Data = [] }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension.Resolution: "contact_administrator" + }); + Assert.Null(response.Suspension.SubscriptionCancelsAt); + } + + [Theory, BitAutoData] + public async Task Run_Has_SuspensionWarning_ContactSupport( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Canceled, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList { Data = [] }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension.Resolution: "contact_support" + }); + } + + [Theory, BitAutoData] + public async Task Run_NotProviderAdmin_NoTaxIdWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(false); + + var response = await sutProvider.Sut.Run(provider); + + Assert.Null(response!.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "CA" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.Null(response!.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdMissingWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + TaxId.Type: "tax_id_missing" + }); + } + + [Theory, BitAutoData] + public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList + { + Data = [new TaxId { Verification = null }] + }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.Null(response!.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdPendingVerificationWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList + { + Data = [new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Pending + } + }] + }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + TaxId.Type: "tax_id_pending_verification" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdFailedVerificationWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList + { + Data = [new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Unverified + } + }] + }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + TaxId.Type: "tax_id_failed_verification" + }); + } + + [Theory, BitAutoData] + public async Task Run_TaxIdVerified_NoTaxIdWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList + { + Data = [new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Verified + } + }] + }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.Null(response!.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_MultipleRegistrations_MatchesCorrectCountry( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "DE" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Is(opt => opt.Status == TaxRegistrationStatus.Active)) + .Returns(new StripeList + { + Data = [ + new Registration { Country = "US" }, + new Registration { Country = "DE" }, + new Registration { Country = "FR" } + ] + }); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Is(opt => opt.Status == TaxRegistrationStatus.Scheduled)) + .Returns(new StripeList { Data = [] }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + TaxId.Type: "tax_id_missing" + }); + } + + [Theory, BitAutoData] + public async Task Run_CombinesBothWarningTypes( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = false; + var cancelAt = DateTime.UtcNow.AddDays(5); + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Unpaid, + CancelAt = cancelAt, + Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "US" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "US" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension.Resolution: "add_payment_method", + TaxId.Type: "tax_id_missing" + }); + Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt); + } +} diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs similarity index 100% rename from bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs similarity index 100% rename from bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs similarity index 100% rename from bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 4915e5ef8e..762b06db96 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -6,7 +6,6 @@ using Bit.Api.Billing.Models.Responses; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; -using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; @@ -28,7 +27,6 @@ public class OrganizationBillingController( ICurrentContext currentContext, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, - IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IPaymentService paymentService, IPricingClient pricingClient, ISubscriberService subscriberService, @@ -359,31 +357,6 @@ public class OrganizationBillingController( return TypedResults.Ok(providerId); } - [HttpGet("warnings")] - public async Task GetWarningsAsync([FromRoute] Guid organizationId) - { - /* - * We'll keep these available at the User level because we're hiding any pertinent information, and - * we want to throw as few errors as possible since these are not core features. - */ - if (!await currentContext.OrganizationUser(organizationId)) - { - return Error.Unauthorized(); - } - - var organization = await organizationRepository.GetByIdAsync(organizationId); - - if (organization == null) - { - return Error.NotFound(); - } - - var warnings = await getOrganizationWarningsQuery.Run(organization); - - return TypedResults.Ok(warnings); - } - - [HttpPost("change-frequency")] [SelfHosted(NotSelfHostedOnly = true)] public async Task ChangePlanSubscriptionFrequencyAsync( diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index 429f2065f6..a85dfe11e1 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -1,9 +1,10 @@ -#nullable enable -using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requirements; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; using Bit.Core.Utilities; @@ -21,6 +22,7 @@ public class OrganizationBillingVNextController( ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, + IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IUpdateBillingAddressCommand updateBillingAddressCommand, IUpdatePaymentMethodCommand updatePaymentMethodCommand, @@ -104,4 +106,14 @@ public class OrganizationBillingVNextController( var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode); return Handle(result); } + + [Authorize] + [HttpGet("warnings")] + [InjectOrganization] + public async Task GetWarningsAsync( + [BindNever] Organization organization) + { + var warnings = await getOrganizationWarningsQuery.Run(organization); + return TypedResults.Ok(warnings); + } } diff --git a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs index d0cc377245..b0b39eaf4a 100644 --- a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs @@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Providers.Queries; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -19,6 +20,7 @@ public class ProviderBillingVNextController( IGetBillingAddressQuery getBillingAddressQuery, IGetCreditQuery getCreditQuery, IGetPaymentMethodQuery getPaymentMethodQuery, + IGetProviderWarningsQuery getProviderWarningsQuery, IProviderService providerService, IUpdateBillingAddressCommand updateBillingAddressCommand, IUpdatePaymentMethodCommand updatePaymentMethodCommand, @@ -104,4 +106,13 @@ public class ProviderBillingVNextController( var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode); return Handle(result); } + + [HttpGet("warnings")] + [InjectProvider(ProviderUserType.ServiceUser)] + public async Task GetWarningsAsync( + [BindNever] Provider provider) + { + var warnings = await getProviderWarningsQuery.Run(provider); + return TypedResults.Ok(warnings); + } } diff --git a/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs b/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs index 4efdf0812a..9978e84f56 100644 --- a/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs +++ b/src/Api/Billing/Models/Requirements/ManageOrganizationBillingRequirement.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization; using Bit.Core.Context; using Bit.Core.Enums; diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 7b4cb3baed..2be88902c8 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -113,6 +113,21 @@ public static class StripeConstants public const string SpanishNIF = "es_cif"; } + public static class TaxIdVerificationStatus + { + public const string Pending = "pending"; + public const string Unavailable = "unavailable"; + public const string Unverified = "unverified"; + public const string Verified = "verified"; + } + + public static class TaxRegistrationStatus + { + public const string Active = "active"; + public const string Expired = "expired"; + public const string Scheduled = "scheduled"; + } + public static class ValidateTaxLocationTiming { public const string Deferred = "deferred"; diff --git a/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs index 4507c84083..cf386fb317 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationWarnings.cs @@ -5,6 +5,7 @@ public record OrganizationWarnings public FreeTrialWarning? FreeTrial { get; set; } public InactiveSubscriptionWarning? InactiveSubscription { get; set; } public ResellerRenewalWarning? ResellerRenewal { get; set; } + public TaxIdWarning? TaxId { get; set; } public record FreeTrialWarning { @@ -39,4 +40,9 @@ public record OrganizationWarnings public required DateTime SuspensionDate { get; set; } } } + + public record TaxIdWarning + { + public required string Type { get; set; } + } } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index a46d7483e7..0b0cbd22c6 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -1,26 +1,25 @@ -// ReSharper disable InconsistentNaming - -using Bit.Core.AdminConsole.Entities; +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.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.Services; using Bit.Core.Context; using Bit.Core.Services; using Stripe; -using FreeTrialWarning = Bit.Core.Billing.Organizations.Models.OrganizationWarnings.FreeTrialWarning; -using InactiveSubscriptionWarning = - Bit.Core.Billing.Organizations.Models.OrganizationWarnings.InactiveSubscriptionWarning; -using ResellerRenewalWarning = - Bit.Core.Billing.Organizations.Models.OrganizationWarnings.ResellerRenewalWarning; +using Stripe.Tax; namespace Bit.Core.Billing.Organizations.Queries; using static StripeConstants; +using FreeTrialWarning = OrganizationWarnings.FreeTrialWarning; +using InactiveSubscriptionWarning = OrganizationWarnings.InactiveSubscriptionWarning; +using ResellerRenewalWarning = OrganizationWarnings.ResellerRenewalWarning; +using TaxIdWarning = OrganizationWarnings.TaxIdWarning; public interface IGetOrganizationWarningsQuery { @@ -38,29 +37,31 @@ public class GetOrganizationWarningsQuery( public async Task Run( Organization organization) { - var response = new OrganizationWarnings(); + var warnings = new OrganizationWarnings(); var subscription = await subscriberService.GetSubscription(organization, - new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] }); + new SubscriptionGetOptions { Expand = ["customer.tax_ids", "latest_invoice", "test_clock"] }); if (subscription == null) { - return response; + return warnings; } - response.FreeTrial = await GetFreeTrialWarning(organization, subscription); + warnings.FreeTrial = await GetFreeTrialWarningAsync(organization, subscription); var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id); - response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription); + warnings.InactiveSubscription = await GetInactiveSubscriptionWarningAsync(organization, provider, subscription); - response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription); + warnings.ResellerRenewal = await GetResellerRenewalWarningAsync(provider, subscription); - return response; + warnings.TaxId = await GetTaxIdWarningAsync(organization, subscription.Customer, provider); + + return warnings; } - private async Task GetFreeTrialWarning( + private async Task GetFreeTrialWarningAsync( Organization organization, Subscription subscription) { @@ -81,7 +82,7 @@ public class GetOrganizationWarningsQuery( var customer = subscription.Customer; - var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); + var hasUnverifiedBankAccount = await HasUnverifiedBankAccountAsync(organization); var hasPaymentMethod = !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || @@ -101,66 +102,51 @@ public class GetOrganizationWarningsQuery( return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays }; } - private async Task GetInactiveSubscriptionWarning( + private async Task GetInactiveSubscriptionWarningAsync( Organization organization, Provider? provider, Subscription subscription) { + // If the organization is enabled or the subscription is active, don't return a warning. + if (organization.Enabled || subscription is not + { + Status: SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled + }) + { + return null; + } + + // If the organization is managed by a provider, return a warning asking them to contact the provider. + if (provider != null) + { + return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; + } + var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id); - switch (organization.Enabled) + /* If the organization is not managed by a provider and this user is the owner, return a warning based + on the subscription status. */ + if (isOrganizationOwner) { - // Member of an enabled, trialing organization. - case true when subscription.Status is SubscriptionStatus.Trialing: + return subscription.Status switch + { + SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning { - var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization); - - var hasPaymentMethod = - !string.IsNullOrEmpty(subscription.Customer.InvoiceSettings.DefaultPaymentMethodId) || - !string.IsNullOrEmpty(subscription.Customer.DefaultSourceId) || - hasUnverifiedBankAccount || - subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId); - - // If this member is the owner and there's no payment method on file, ask them to add one. - return isOrganizationOwner && !hasPaymentMethod - ? new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" } - : null; - } - // Member of disabled and unpaid or canceled organization. - case false when subscription.Status is SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled: + Resolution = "add_payment_method" + }, + SubscriptionStatus.Canceled => new InactiveSubscriptionWarning { - // If the organization is managed by a provider, return a warning asking them to contact the provider. - if (provider != null) - { - return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; - } - - /* If the organization is not managed by a provider and this user is the owner, return an action warning based - on the subscription status. */ - if (isOrganizationOwner) - { - return subscription.Status switch - { - SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning - { - Resolution = "add_payment_method" - }, - SubscriptionStatus.Canceled => new InactiveSubscriptionWarning - { - Resolution = "resubscribe" - }, - _ => null - }; - } - - // Otherwise, this member is not the owner, and we need to ask them to contact the owner. - return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; - } - default: return null; + Resolution = "resubscribe" + }, + _ => null + }; } + + // Otherwise, return a warning asking them to contact the owner. + return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; } - private async Task GetResellerRenewalWarning( + private async Task GetResellerRenewalWarningAsync( Provider? provider, Subscription subscription) { @@ -241,7 +227,62 @@ public class GetOrganizationWarningsQuery( return null; } - private async Task HasUnverifiedBankAccount( + private async Task GetTaxIdWarningAsync( + Organization organization, + Customer customer, + Provider? provider) + { + var productTier = organization.PlanType.GetProductTier(); + + // Only business tier customers can have tax IDs + if (productTier is not ProductTierType.Teams and not ProductTierType.Enterprise) + { + return null; + } + + // Only an organization owner can update a tax ID + if (!await currentContext.OrganizationOwner(organization.Id)) + { + return null; + } + + if (provider != null) + { + return null; + } + + // Get active and scheduled registrations + var registrations = (await Task.WhenAll( + stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Active }), + stripeAdapter.TaxRegistrationsListAsync(new RegistrationListOptions { Status = TaxRegistrationStatus.Scheduled }))) + .SelectMany(registrations => registrations.Data); + + // Find the matching registration for the customer + var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country); + + // If we're not registered in their country, we don't need a warning + if (registration == null) + { + return null; + } + + var taxId = customer.TaxIds.FirstOrDefault(); + + return taxId switch + { + // Customer's tax ID is missing + null => new TaxIdWarning { Type = "tax_id_missing" }, + // Not sure if this case is valid, but Stripe says this property is nullable + not null when taxId.Verification == null => null, + // Customer's tax ID is pending verification + not null when taxId.Verification.Status == TaxIdVerificationStatus.Pending => new TaxIdWarning { Type = "tax_id_pending_verification" }, + // Customer's tax ID failed verification + not null when taxId.Verification.Status == TaxIdVerificationStatus.Unverified => new TaxIdWarning { Type = "tax_id_failed_verification" }, + _ => null + }; + } + + private async Task HasUnverifiedBankAccountAsync( Organization organization) { var setupIntentId = await setupIntentCache.Get(organization.Id); diff --git a/src/Core/Billing/Providers/Models/ProviderWarnings.cs b/src/Core/Billing/Providers/Models/ProviderWarnings.cs new file mode 100644 index 0000000000..dd9d9be41c --- /dev/null +++ b/src/Core/Billing/Providers/Models/ProviderWarnings.cs @@ -0,0 +1,18 @@ +namespace Bit.Core.Billing.Providers.Models; + +public class ProviderWarnings +{ + public SuspensionWarning? Suspension { get; set; } + public TaxIdWarning? TaxId { get; set; } + + public record SuspensionWarning + { + public required string Resolution { get; set; } + public DateTime? SubscriptionCancelsAt { get; set; } + } + + public record TaxIdWarning + { + public required string Type { get; set; } + } +} diff --git a/src/Core/Billing/Providers/Queries/IGetProviderWarningsQuery.cs b/src/Core/Billing/Providers/Queries/IGetProviderWarningsQuery.cs new file mode 100644 index 0000000000..ed868a8475 --- /dev/null +++ b/src/Core/Billing/Providers/Queries/IGetProviderWarningsQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Providers.Models; + +namespace Bit.Core.Billing.Providers.Queries; + +public interface IGetProviderWarningsQuery +{ + Task Run(Provider provider); +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7cedf42b5b..d4a6fdb31d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -161,6 +161,7 @@ public static class FeatureFlagKeys public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; + public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; /* Key Management Team */ public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index 2b2bf8d825..8a41263956 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -48,6 +48,7 @@ public interface IStripeAdapter 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); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 9315d92ebe..03d1776e90 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -22,6 +22,7 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.SetupIntentService _setupIntentService; private readonly Stripe.TestHelpers.TestClockService _testClockService; private readonly CustomerBalanceTransactionService _customerBalanceTransactionService; + private readonly Stripe.Tax.RegistrationService _taxRegistrationService; public StripeAdapter() { @@ -39,6 +40,7 @@ public class StripeAdapter : IStripeAdapter _setupIntentService = new SetupIntentService(); _testClockService = new Stripe.TestHelpers.TestClockService(); _customerBalanceTransactionService = new CustomerBalanceTransactionService(); + _taxRegistrationService = new Stripe.Tax.RegistrationService(); } public Task CustomerCreateAsync(Stripe.CustomerCreateOptions options) @@ -208,6 +210,11 @@ public class StripeAdapter : IStripeAdapter return _taxIdService.DeleteAsync(customerId, taxIdId); } + public Task> TaxRegistrationsListAsync(Stripe.Tax.RegistrationListOptions options = null) + { + return _taxRegistrationService.ListAsync(options); + } + public Task> ChargeListAsync(Stripe.ChargeListOptions options) { return _chargeService.ListAsync(options); diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 54c982192b..c22cc239d8 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -21,7 +21,7 @@ namespace Bit.Core.Test.Billing.Organizations.Queries; [SutProviderCustomize] public class GetOrganizationWarningsQueryTests { - private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"]; + private static readonly string[] _requiredExpansions = ["customer.tax_ids", "latest_invoice", "test_clock"]; [Theory, BitAutoData] public async Task Run_NoSubscription_NoWarnings( @@ -130,7 +130,7 @@ public class GetOrganizationWarningsQueryTests } [Theory, BitAutoData] - public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethodOptionalTrial( + public async Task Run_OrganizationEnabled_NoInactiveSubscriptionWarning( Organization organization, SutProvider sutProvider) { @@ -142,7 +142,7 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Trialing, + Status = StripeConstants.SubscriptionStatus.Unpaid, Customer = new Customer { InvoiceSettings = new CustomerInvoiceSettings(), @@ -151,14 +151,10 @@ public class GetOrganizationWarningsQueryTests }); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); - sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); var response = await sutProvider.Sut.Run(organization); - Assert.True(response is - { - InactiveSubscription.Resolution: "add_payment_method_optional_trial" - }); + Assert.Null(response.InactiveSubscription); } [Theory, BitAutoData] From 03327cb082a35ede6f61191afa140ddf2297aa97 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Mon, 18 Aug 2025 11:12:42 -0400 Subject: [PATCH 145/326] [PM-24278] Fix sproc to return UserId (#6203) --- .../PolicyDetails_ReadByOrganizationId.sql | 1 + ...PolicyDetailsByOrganizationIdAsyncTests.cs | 63 ++++++++++++++ ...Details_ReadByOrganizationId_AddUserId.sql | 82 +++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 util/Migrator/DbScripts/2025-08-15_00_PolicyDetails_ReadByOrganizationId_AddUserId.sql diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql index 526a9141ac..3a93687d25 100644 --- a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByOrganizationId.sql @@ -52,6 +52,7 @@ BEGIN -- Return policy details for each matching organization user. SELECT OU.[OrganizationUserId], + OU.[UserId], P.[OrganizationId], P.[Type] AS [PolicyType], P.[Data] AS [PolicyData], diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs index 7dc4b6d2b3..e1352f5c8b 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByOrganizationIdAsyncTests.cs @@ -40,6 +40,10 @@ public class GetPolicyDetailsByOrganizationIdAsyncTests Assert.True(results.Single().IsProvider); + // Annul + await organizationRepository.DeleteAsync(new Organization { Id = userOrgConnectedDirectly.OrganizationId }); + await userRepository.DeleteAsync(user); + async Task ArrangeProvider() { var provider = await providerRepository.CreateAsync(new Provider @@ -86,6 +90,11 @@ public class GetPolicyDetailsByOrganizationIdAsyncTests Assert.Contains(results, result => result.OrganizationUserId == userOrgConnectedDirectly.Id && result.OrganizationId == userOrgConnectedDirectly.OrganizationId); Assert.DoesNotContain(results, result => result.OrganizationId == notConnectedOrg.Id); + + // Annul + await organizationRepository.DeleteAsync(new Organization { Id = userOrgConnectedDirectly.OrganizationId }); + await organizationRepository.DeleteAsync(notConnectedOrg); + await userRepository.DeleteAsync(user); } [DatabaseTheory, DatabaseData] @@ -115,6 +124,10 @@ public class GetPolicyDetailsByOrganizationIdAsyncTests && result.PolicyType == inputPolicyType); Assert.DoesNotContain(results, result => result.PolicyType == notInputPolicyType); + + // Annul + await organizationRepository.DeleteAsync(new Organization { Id = orgUser.OrganizationId }); + await userRepository.DeleteAsync(user); } @@ -143,6 +156,12 @@ public class GetPolicyDetailsByOrganizationIdAsyncTests Assert.Equal(expectedCount, results.Count); AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + + // Annul + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedDirectly.OrganizationId }); + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedByEmail.OrganizationId }); + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedByUserId.OrganizationId }); + await userRepository.DeleteAsync(user); } [DatabaseTheory, DatabaseData] @@ -167,8 +186,52 @@ public class GetPolicyDetailsByOrganizationIdAsyncTests // Assert AssertPolicyDetailUserConnections(results, userOrgConnectedDirectly, userOrgConnectedByEmail, userOrgConnectedByUserId); + + // Annul + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedDirectly.OrganizationId }); + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedByEmail.OrganizationId }); + await organizationRepository.DeleteAsync(new Organization() { Id = userOrgConnectedByUserId.OrganizationId }); + await userRepository.DeleteAsync(user); } + [DatabaseTheory, DatabaseData] + public async Task ShouldReturnUserIds( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + const PolicyType policyType = PolicyType.SingleOrg; + + var organization = await CreateEnterpriseOrg(organizationRepository); + await policyRepository.CreateAsync(new Policy { OrganizationId = organization.Id, Enabled = true, Type = policyType }); + + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + // Act + var results = (await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organization.Id, policyType)).ToList(); + + // Assert + Assert.Equal(2, results.Count); + + Assert.Contains(results, result => result.OrganizationUserId == orgUser1.Id + && result.UserId == orgUser1.UserId + && result.OrganizationId == orgUser1.OrganizationId); + + Assert.Contains(results, result => result.OrganizationUserId == orgUser2.Id + && result.UserId == orgUser2.UserId + && result.OrganizationId == orgUser2.OrganizationId); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user1, user2]); + } + + private async Task ArrangeOtherOrgConnectedByUserIdAsync(IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IPolicyRepository policyRepository, User user, PolicyType policyType) diff --git a/util/Migrator/DbScripts/2025-08-15_00_PolicyDetails_ReadByOrganizationId_AddUserId.sql b/util/Migrator/DbScripts/2025-08-15_00_PolicyDetails_ReadByOrganizationId_AddUserId.sql new file mode 100644 index 0000000000..0e4dde6e02 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-15_00_PolicyDetails_ReadByOrganizationId_AddUserId.sql @@ -0,0 +1,82 @@ +CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @PolicyType TINYINT +AS +BEGIN + SET NOCOUNT ON; + + -- Get users in the given organization (@OrganizationId) by matching either on UserId or Email. + ;WITH GivenOrgUsers AS ( + SELECT + OU.[UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Id] = OU.[UserId] + WHERE OU.[OrganizationId] = @OrganizationId + + UNION ALL + + SELECT + U.[Id] AS [UserId], + U.[Email] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] + WHERE OU.[OrganizationId] = @OrganizationId + ), + + -- Retrieve all organization users that match on either UserId or Email from GivenOrgUsers. + AllOrgUsers AS ( + SELECT + OU.[Id] AS [OrganizationUserId], + OU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[UserId] = OU.[UserId] + UNION ALL + SELECT + OU.[Id] AS [OrganizationUserId], + AU.[UserId], + OU.[OrganizationId], + AU.[Email], + OU.[Type] AS [OrganizationUserType], + OU.[Status] AS [OrganizationUserStatus], + OU.[Permissions] AS [OrganizationUserPermissionsData] + FROM [dbo].[OrganizationUserView] OU + INNER JOIN GivenOrgUsers AU ON AU.[Email] = OU.[Email] + ) + + -- Return policy details for each matching organization user. + SELECT + OU.[OrganizationUserId], + OU.[UserId], + P.[OrganizationId], + P.[Type] AS [PolicyType], + P.[Data] AS [PolicyData], + OU.[OrganizationUserType], + OU.[OrganizationUserStatus], + OU.[OrganizationUserPermissionsData], + -- Check if user is a provider for the organization + 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].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN AllOrgUsers OU ON OU.[OrganizationId] = O.[Id] + WHERE P.[Enabled] = 1 + AND O.[Enabled] = 1 + AND O.[UsePolicies] = 1 + AND P.[Type] = @PolicyType + +END +GO \ No newline at end of file From 6971f0a976eef71df84224ece664d014f56fccd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 18 Aug 2025 18:40:50 +0200 Subject: [PATCH 146/326] Update Swashbuckle and improve generated OpenAPI files (#6066) * Improve generated OpenAPI files * Nullable * Fmt * Correct powershell command * Fix name * Add some tests * Fmt * Switch to using json naming policy --- .config/dotnet-tools.json | 2 +- .github/workflows/build.yml | 49 ++--------- .gitignore | 7 +- bitwarden-server.sln | 7 ++ dev/generate_openapi_files.ps1 | 19 ++++ src/Api/Api.csproj | 2 +- src/Api/Startup.cs | 2 +- .../Utilities/ServiceCollectionExtensions.cs | 10 ++- src/Billing/Billing.csproj | 2 +- src/Identity/Startup.cs | 15 +++- src/SharedWeb/SharedWeb.csproj | 2 +- .../Swagger/EncryptedStringSchemaFilter.cs | 40 +++++++++ .../Swagger/GitCommitDocumentFilter.cs | 50 +++++++++++ .../Swagger/SourceFileLineOperationFilter.cs | 87 +++++++++++++++++++ .../EncryptedStringSchemaFilterTest.cs | 60 +++++++++++++ test/SharedWeb.Test/EnumSchemaFilterTest.cs | 41 +++++++++ .../GitCommitDocumentFilterTest.cs | 23 +++++ test/SharedWeb.Test/GlobalUsings.cs | 1 + test/SharedWeb.Test/SharedWeb.Test.csproj | 22 +++++ .../SourceFileLineOperationFilterTest.cs | 33 +++++++ 20 files changed, 420 insertions(+), 54 deletions(-) create mode 100644 dev/generate_openapi_files.ps1 create mode 100644 src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs create mode 100644 src/SharedWeb/Swagger/GitCommitDocumentFilter.cs create mode 100644 src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs create mode 100644 test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs create mode 100644 test/SharedWeb.Test/EnumSchemaFilterTest.cs create mode 100644 test/SharedWeb.Test/GitCommitDocumentFilterTest.cs create mode 100644 test/SharedWeb.Test/GlobalUsings.cs create mode 100644 test/SharedWeb.Test/SharedWeb.Test.csproj create mode 100644 test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d7814849c6..41674ccad0 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "7.3.2", + "version": "9.0.2", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54c31ee6ea..1d08145b5a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -376,62 +376,23 @@ jobs: path: docker-stub-EU.zip if-no-files-found: error - - name: Build Public API Swagger + - name: Build Swagger files run: | - cd ./src/Api - echo "Restore tools" - dotnet tool restore - echo "Publish" - dotnet publish -c "Release" -o obj/build-output/publish - - dotnet swagger tofile --output ../../swagger.json --host https://api.bitwarden.com \ - ./obj/build-output/publish/Api.dll public - cd ../.. - env: - ASPNETCORE_ENVIRONMENT: Production - swaggerGen: "True" - DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2 - GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" + cd ./dev + pwsh ./generate_openapi_files.ps1 - name: Upload Public API Swagger artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: swagger.json - path: swagger.json + path: api.public.json if-no-files-found: error - - name: Build Internal API Swagger - run: | - cd ./src/Api - echo "Restore API tools" - dotnet tool restore - echo "Publish API" - dotnet publish -c "Release" -o obj/build-output/publish - - dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \ - ./obj/build-output/publish/Api.dll internal - - cd ../Identity - - echo "Restore Identity tools" - dotnet tool restore - echo "Publish Identity" - dotnet publish -c "Release" -o obj/build-output/publish - - dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \ - ./obj/build-output/publish/Identity.dll v1 - cd ../.. - env: - ASPNETCORE_ENVIRONMENT: Development - swaggerGen: "True" - DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2 - GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder" - - name: Upload Internal API Swagger artifact uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 with: name: internal.json - path: internal.json + path: api.json if-no-files-found: error - name: Upload Identity Swagger artifact diff --git a/.gitignore b/.gitignore index e1b2153433..3b1f40e673 100644 --- a/.gitignore +++ b/.gitignore @@ -129,7 +129,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj @@ -226,3 +226,8 @@ src/Notifications/Notifications.zip bitwarden_license/src/Portal/Portal.zip bitwarden_license/src/Sso/Sso.zip **/src/**/flags.json + +# Generated swagger specs +/identity.json +/api.json +/api.public.json diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 2ec8d86e0e..dbc37372a1 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -133,6 +133,8 @@ 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}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -337,6 +339,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 + {AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -391,6 +397,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} + {AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/dev/generate_openapi_files.ps1 b/dev/generate_openapi_files.ps1 new file mode 100644 index 0000000000..02470a0b1d --- /dev/null +++ b/dev/generate_openapi_files.ps1 @@ -0,0 +1,19 @@ +Set-Location "$PSScriptRoot/.." + +$env:ASPNETCORE_ENVIRONMENT = "Development" +$env:swaggerGen = "True" +$env:DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX = "2" +$env:GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING = "placeholder" + +dotnet tool restore + +# Identity +Set-Location "./src/Identity" +dotnet build +dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1" + +# Api internal & public +Set-Location "../../src/Api" +dotnet build +dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal" +dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public" diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 11af4d5e0a..d48f49626f 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -34,7 +34,7 @@ - + diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 699fa3f804..450cb64bad 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -210,7 +210,7 @@ public class Startup config.Conventions.Add(new PublicApiControllersModelConvention()); }); - services.AddSwagger(globalSettings); + services.AddSwagger(globalSettings, Environment); Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted); services.AddHostedService(); diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 4f123d3f4f..aa2710c42a 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ namespace Bit.Api.Utilities; public static class ServiceCollectionExtensions { - public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings) + public static void AddSwagger(this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment) { services.AddSwaggerGen(config => { @@ -83,6 +83,14 @@ public static class ServiceCollectionExtensions // config.UseReferencedDefinitionsForEnums(); config.SchemaFilter(); + config.SchemaFilter(); + + // These two filters require debug symbols/git, so only add them in development mode + if (environment.IsDevelopment()) + { + config.DocumentFilter(); + config.OperationFilter(); + } var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml"); config.IncludeXmlComments(apiFilePath, true); diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 25327b17b7..18c627c5de 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index baaf9385af..ae628197e8 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -64,10 +64,19 @@ public class Startup config.Filters.Add(new ModelStateValidationFilterAttribute()); }); - services.AddSwaggerGen(c => + services.AddSwaggerGen(config => { - c.SchemaFilter(); - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" }); + config.SchemaFilter(); + config.SchemaFilter(); + + // These two filters require debug symbols/git, so only add them in development mode + if (Environment.IsDevelopment()) + { + config.DocumentFilter(); + config.OperationFilter(); + } + + config.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" }); }); if (!globalSettings.SelfHosted) diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index 1951e4d509..445b98cce0 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs b/src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs new file mode 100644 index 0000000000..d26ae58e59 --- /dev/null +++ b/src/SharedWeb/Swagger/EncryptedStringSchemaFilter.cs @@ -0,0 +1,40 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Utilities; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +/// +/// Set the format of any strings that are decorated with the to "x-enc-string". +/// This will allow the generated bindings to use a more appropriate type for encrypted strings. +/// +public class EncryptedStringSchemaFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (context.Type == null || schema.Properties == null) + return; + + foreach (var prop in context.Type.GetProperties()) + { + // Only apply to string properties + if (prop.PropertyType != typeof(string)) + continue; + + // Check if the property has the EncryptedString attribute + if (prop.GetCustomAttributes(typeof(EncryptedStringAttribute), true).FirstOrDefault() != null) + { + // Convert prop.Name to camelCase for JSON schema property lookup + var jsonPropName = JsonNamingPolicy.CamelCase.ConvertName(prop.Name); + + if (schema.Properties.TryGetValue(jsonPropName, out var value)) + { + value.Format = "x-enc-string"; + } + } + } + } +} diff --git a/src/SharedWeb/Swagger/GitCommitDocumentFilter.cs b/src/SharedWeb/Swagger/GitCommitDocumentFilter.cs new file mode 100644 index 0000000000..86678722ce --- /dev/null +++ b/src/SharedWeb/Swagger/GitCommitDocumentFilter.cs @@ -0,0 +1,50 @@ +#nullable enable + +using System.Diagnostics; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +/// +/// Add the Git commit that was used to generate the Swagger document, to help with debugging and reproducibility. +/// +public class GitCommitDocumentFilter : IDocumentFilter +{ + + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + if (!string.IsNullOrEmpty(GitCommit)) + { + swaggerDoc.Extensions.Add("x-git-commit", new Microsoft.OpenApi.Any.OpenApiString(GitCommit)); + } + } + + public static string? GitCommit => _gitCommit.Value; + + private static readonly Lazy _gitCommit = new(() => + { + try + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "git", + Arguments = "rev-parse HEAD", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + process.Start(); + var result = process.StandardOutput.ReadLine()?.Trim(); + process.WaitForExit(); + return result ?? string.Empty; + } + catch + { + return null; + } + }); +} diff --git a/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs new file mode 100644 index 0000000000..cbad1e9736 --- /dev/null +++ b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs @@ -0,0 +1,87 @@ +#nullable enable + +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Runtime.CompilerServices; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +/// +/// Adds source file and line number information to the Swagger operation description. +/// This can be useful for locating the source code of the operation in the repository, +/// as the generated names are based on the HTTP path, and are hard to search for. +/// +public class SourceFileLineOperationFilter : IOperationFilter +{ + private static readonly string _gitCommit = GitCommitDocumentFilter.GitCommit ?? "main"; + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + + var (fileName, lineNumber) = GetSourceFileLine(context.MethodInfo); + if (fileName != null && lineNumber > 0) + { + // Add the information with a link to the source file at the end of the operation description + operation.Description += + $"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/{_gitCommit}/{fileName}#L{lineNumber}`]"; + + // Also add the information as extensions, so other tools can use it in the future + operation.Extensions.Add("x-source-file", new OpenApiString(fileName)); + operation.Extensions.Add("x-source-line", new OpenApiInteger(lineNumber)); + } + } + + private static (string? fileName, int lineNumber) GetSourceFileLine(MethodInfo methodInfo) + { + // Get the location of the PDB file associated with the module of the method + var pdbPath = Path.ChangeExtension(methodInfo.Module.FullyQualifiedName, ".pdb"); + if (!File.Exists(pdbPath)) return (null, 0); + + // Open the PDB file and read the metadata + using var pdbStream = File.OpenRead(pdbPath); + using var metadataReaderProvider = MetadataReaderProvider.FromPortablePdbStream(pdbStream); + var metadataReader = metadataReaderProvider.GetMetadataReader(); + + // If the method is async, the compiler will generate a state machine, + // so we can't look for the original method, but we instead need to look + // for the MoveNext method of the state machine. + var attr = methodInfo.GetCustomAttribute(); + if (attr?.StateMachineType != null) + { + var moveNext = attr.StateMachineType.GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (moveNext != null) methodInfo = moveNext; + } + + // Once we have the method, we can get its sequence points + var handle = (MethodDefinitionHandle)MetadataTokens.Handle(methodInfo.MetadataToken); + if (handle.IsNil) return (null, 0); + var sequencePoints = metadataReader.GetMethodDebugInformation(handle).GetSequencePoints(); + + // Iterate through the sequence points and pick the first one that has a valid line number + foreach (var sp in sequencePoints) + { + var docName = metadataReader.GetDocument(sp.Document).Name; + if (sp.StartLine != 0 && sp.StartLine != SequencePoint.HiddenLine && !docName.IsNil) + { + var fileName = metadataReader.GetString(docName); + var repoRoot = FindRepoRoot(AppContext.BaseDirectory); + var relativeFileName = repoRoot != null ? Path.GetRelativePath(repoRoot, fileName) : fileName; + return (relativeFileName, sp.StartLine); + } + } + + return (null, 0); + } + + private static string? FindRepoRoot(string startPath) + { + var dir = new DirectoryInfo(startPath); + while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, ".git"))) + dir = dir.Parent; + return dir?.FullName; + } +} diff --git a/test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs b/test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs new file mode 100644 index 0000000000..172ddf5ee5 --- /dev/null +++ b/test/SharedWeb.Test/EncryptedStringSchemaFilterTest.cs @@ -0,0 +1,60 @@ +using Bit.Core.Utilities; +using Bit.SharedWeb.Swagger; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + + +namespace SharedWeb.Test; + +public class EncryptedStringSchemaFilterTest +{ + private class TestClass + { + [EncryptedString] + public string SecretKey { get; set; } + + public string Username { get; set; } + + [EncryptedString] + public int Wrong { get; set; } + } + + [Fact] + public void AnnotatedStringSetsFormat() + { + var schema = new OpenApiSchema + { + Properties = new Dictionary { { "secretKey", new() } } + }; + var context = new SchemaFilterContext(typeof(TestClass), null, null, null); + var filter = new EncryptedStringSchemaFilter(); + filter.Apply(schema, context); + Assert.Equal("x-enc-string", schema.Properties["secretKey"].Format); + } + + [Fact] + public void NonAnnotatedStringIsIgnored() + { + var schema = new OpenApiSchema + { + Properties = new Dictionary { { "username", new() } } + }; + var context = new SchemaFilterContext(typeof(TestClass), null, null, null); + var filter = new EncryptedStringSchemaFilter(); + filter.Apply(schema, context); + Assert.Null(schema.Properties["username"].Format); + } + + [Fact] + public void AnnotatedWrongTypeIsIgnored() + { + var schema = new OpenApiSchema + { + Properties = new Dictionary { { "wrong", new() } } + }; + var context = new SchemaFilterContext(typeof(TestClass), null, null, null); + var filter = new EncryptedStringSchemaFilter(); + filter.Apply(schema, context); + Assert.Null(schema.Properties["wrong"].Format); + } +} diff --git a/test/SharedWeb.Test/EnumSchemaFilterTest.cs b/test/SharedWeb.Test/EnumSchemaFilterTest.cs new file mode 100644 index 0000000000..b0c14437c1 --- /dev/null +++ b/test/SharedWeb.Test/EnumSchemaFilterTest.cs @@ -0,0 +1,41 @@ +using Bit.SharedWeb.Swagger; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class EnumSchemaFilterTest +{ + private enum TestEnum + { + First, + Second, + Third + } + + [Fact] + public void SetsEnumVarNamesExtension() + { + var schema = new OpenApiSchema(); + var context = new SchemaFilterContext(typeof(TestEnum), null, null, null); + var filter = new EnumSchemaFilter(); + filter.Apply(schema, context); + + Assert.True(schema.Extensions.ContainsKey("x-enum-varnames")); + var extensions = schema.Extensions["x-enum-varnames"] as OpenApiArray; + Assert.NotNull(extensions); + Assert.Equal(["First", "Second", "Third"], extensions.Select(x => ((OpenApiString)x).Value)); + } + + [Fact] + public void DoesNotSetExtensionForNonEnum() + { + var schema = new OpenApiSchema(); + var context = new SchemaFilterContext(typeof(string), null, null, null); + var filter = new EnumSchemaFilter(); + filter.Apply(schema, context); + + Assert.False(schema.Extensions.ContainsKey("x-enum-varnames")); + } +} diff --git a/test/SharedWeb.Test/GitCommitDocumentFilterTest.cs b/test/SharedWeb.Test/GitCommitDocumentFilterTest.cs new file mode 100644 index 0000000000..542ef888f9 --- /dev/null +++ b/test/SharedWeb.Test/GitCommitDocumentFilterTest.cs @@ -0,0 +1,23 @@ +using Bit.SharedWeb.Swagger; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class GitCommitDocumentFilterTest +{ + [Fact] + public void AddsGitCommitExtensionIfAvailable() + { + var doc = new OpenApiDocument(); + var context = new DocumentFilterContext(null, null, null); + var filter = new GitCommitDocumentFilter(); + filter.Apply(doc, context); + + Assert.True(doc.Extensions.ContainsKey("x-git-commit")); + var ext = doc.Extensions["x-git-commit"] as Microsoft.OpenApi.Any.OpenApiString; + Assert.NotNull(ext); + Assert.False(string.IsNullOrEmpty(ext.Value)); + + } +} diff --git a/test/SharedWeb.Test/GlobalUsings.cs b/test/SharedWeb.Test/GlobalUsings.cs new file mode 100644 index 0000000000..9df1d42179 --- /dev/null +++ b/test/SharedWeb.Test/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/test/SharedWeb.Test/SharedWeb.Test.csproj b/test/SharedWeb.Test/SharedWeb.Test.csproj new file mode 100644 index 0000000000..8ae7a56a99 --- /dev/null +++ b/test/SharedWeb.Test/SharedWeb.Test.csproj @@ -0,0 +1,22 @@ + + + false + SharedWeb.Test + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + diff --git a/test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs b/test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs new file mode 100644 index 0000000000..98da92c8c1 --- /dev/null +++ b/test/SharedWeb.Test/SourceFileLineOperationFilterTest.cs @@ -0,0 +1,33 @@ +using Bit.SharedWeb.Swagger; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class SourceFileLineOperationFilterTest +{ + private class DummyController + { + public void DummyMethod() { } + } + + [Fact] + public void AddsSourceFileAndLineExtensionsIfAvailable() + { + var methodInfo = typeof(DummyController).GetMethod(nameof(DummyController.DummyMethod)); + var operation = new OpenApiOperation(); + var context = new OperationFilterContext(null, null, null, methodInfo); + var filter = new SourceFileLineOperationFilter(); + filter.Apply(operation, context); + + Assert.True(operation.Extensions.ContainsKey("x-source-file")); + Assert.True(operation.Extensions.ContainsKey("x-source-line")); + var fileExt = operation.Extensions["x-source-file"] as Microsoft.OpenApi.Any.OpenApiString; + var lineExt = operation.Extensions["x-source-line"] as Microsoft.OpenApi.Any.OpenApiInteger; + Assert.NotNull(fileExt); + Assert.NotNull(lineExt); + + Assert.Equal(11, lineExt.Value); + Assert.StartsWith("test/SharedWeb.Test/", fileExt.Value); + } +} From ae1e9a2aedf2d2a158926a79a7c0bfb48bf43278 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Mon, 18 Aug 2025 15:25:40 -0400 Subject: [PATCH 147/326] [PM-24556] Remove Code for PM-21383 Get Provider Price from Stripe (#6217) * refactor: remove flag in controller * tests: remove flag use in test * refactor: remove flag constant --- .../Controllers/ProviderBillingController.cs | 24 ++++--------------- src/Core/Constants.cs | 1 - .../ProviderBillingControllerTests.cs | 4 ---- 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 80b145a2e0..c131ed7688 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -4,7 +4,6 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Commercial.Core.Billing.Providers.Services; -using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Models; @@ -27,7 +26,6 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class ProviderBillingController( ICurrentContext currentContext, - IFeatureService featureService, ILogger logger, IPricingClient pricingClient, IProviderBillingService providerBillingService, @@ -139,27 +137,15 @@ public class ProviderBillingController( var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); - var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe); - var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => { var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type); + var price = await stripeAdapter.PriceGetAsync(priceId); - decimal unitAmount; - - if (getProviderPriceFromStripe) - { - var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type); - var price = await stripeAdapter.PriceGetAsync(priceId); - - unitAmount = price.UnitAmountDecimal.HasValue - ? price.UnitAmountDecimal.Value / 100M - : plan.PasswordManager.ProviderPortalSeatPrice; - } - else - { - unitAmount = plan.PasswordManager.ProviderPortalSeatPrice; - } + var unitAmount = price.UnitAmountDecimal.HasValue + ? price.UnitAmountDecimal.Value / 100M + : plan.PasswordManager.ProviderPortalSeatPrice; return new ConfiguredProviderPlan( providerPlan.Id, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d4a6fdb31d..81b7c59259 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -158,7 +158,6 @@ public static class FeatureFlagKeys public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; - public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index a082caa469..75f301ec9c 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -2,7 +2,6 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Commercial.Core.Billing.Providers.Services; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -346,9 +345,6 @@ public class ProviderBillingControllerTests } }; - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe) - .Returns(true); - sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); foreach (var providerPlan in providerPlans) From 29d6288b2772d045565c7dcc086a7b96fd1ba93b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:53:54 +0100 Subject: [PATCH 148/326] Add the expiration date (#6191) --- src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs | 3 +-- .../Implementations/OrganizationLicenseClaimsFactory.cs | 3 +-- src/Core/Billing/Organizations/Models/OrganizationLicense.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index 9ac1ace156..f5b4499ea8 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -3,7 +3,6 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; namespace Bit.Core.Billing.Licenses.Extensions; @@ -14,7 +13,7 @@ public static class LicenseExtensions { if (subscriptionInfo?.Subscription == null) { - if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) + if (org.ExpirationDate.HasValue) { return org.ExpirationDate.Value; } diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 678ac7f97e..02b35583af 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Security.Claims; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Enums; using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Licenses.Models; using Bit.Core.Enums; @@ -121,6 +120,6 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory subscriptionInfo?.Subscription is null - ? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue + ? !org.ExpirationDate.HasValue : subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow; } diff --git a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs index cd90cb517e..54e20cd636 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs @@ -98,7 +98,7 @@ public class OrganizationLicense : ILicense if (subscriptionInfo?.Subscription == null) { - if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) + if (org.ExpirationDate.HasValue) { Expires = Refresh = org.ExpirationDate.Value; Trial = false; From c189e4aaf55fda7dbcf7ce75b41bb1e02b470cdb Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 19 Aug 2025 14:12:34 -0400 Subject: [PATCH 149/326] [PM-22104] Migrate default collection when org user is removed (#6135) * migrate default collection to a shared collection when users are removed * remove redundant logic * fix test * fix tests * fix test * clean up * add migrations * run dotnet format * clean up, refactor duplicate logic to sproc, wip integration test * fix sql * add migration for new sproc * integration test wip * integration test wip * integration test wip * integration test wip * fix integration test LINQ expression * fix using wrong Id * wip integration test for DeleteManyAsync * fix LINQ * only set DefaultUserEmail when it is null in sproc * check for null * spelling, separate create and update request models * fix test * fix child class * refactor sproc * clean up * more cleanup * fix tests * fix user email * remove unneccesary test * add DefaultUserCollectionEmail to EF query * fix test * fix EF logic to match sprocs * clean up logic * cleanup --- src/Api/Controllers/CollectionsController.cs | 4 +- .../Models/Request/CollectionRequestModel.cs | 22 +- .../Response/CollectionResponseModel.cs | 1 + .../OrganizationUserRepository.cs | 210 +++++-- .../Repositories/CollectionRepository.cs | 12 +- .../Queries/CollectionAdminDetailsQuery.cs | 1 + .../OrganizationUser_DeleteById.sql | 7 +- .../OrganizationUser_DeleteByIds.sql | 3 + ...anizationUser_MigrateDefaultCollection.sql | 22 + .../Controllers/CollectionsControllerTests.cs | 179 +++++- .../OrganizationRepositoryTests.cs | 4 +- .../OrganizationUserRepositoryTests.cs | 547 ++++++++++++++---- .../Auth/Repositories/UserRepositoryTests.cs | 2 +- ...4-00_OrgUsers_MigrateDefaultCollection.sql | 22 + .../2025-08-04-01_OrgUsers_DeleteById.sql | 55 ++ .../2025-08-04-02_OrgUsers_DeleteByIds.sql | 105 ++++ 16 files changed, 1001 insertions(+), 195 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_MigrateDefaultCollection.sql create mode 100644 util/Migrator/DbScripts/2025-08-04-00_OrgUsers_MigrateDefaultCollection.sql create mode 100644 util/Migrator/DbScripts/2025-08-04-01_OrgUsers_DeleteById.sql create mode 100644 util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 6708a66326..6d4e9c9fea 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -146,7 +146,7 @@ public class CollectionsController : Controller } [HttpPost("")] - public async Task Post(Guid orgId, [FromBody] CollectionRequestModel model) + public async Task Post(Guid orgId, [FromBody] CreateCollectionRequestModel model) { var collection = model.ToCollection(orgId); @@ -174,7 +174,7 @@ public class CollectionsController : Controller [HttpPut("{id}")] [HttpPost("{id}")] - public async Task Put(Guid orgId, Guid id, [FromBody] CollectionRequestModel model) + public async Task Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model) { var collection = await _collectionRepository.GetByIdAsync(id); var authorized = (await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.Update)).Succeeded; diff --git a/src/Api/Models/Request/CollectionRequestModel.cs b/src/Api/Models/Request/CollectionRequestModel.cs index 9aa80b859b..6e73c37db6 100644 --- a/src/Api/Models/Request/CollectionRequestModel.cs +++ b/src/Api/Models/Request/CollectionRequestModel.cs @@ -7,7 +7,7 @@ using Bit.Core.Utilities; namespace Bit.Api.Models.Request; -public class CollectionRequestModel +public class CreateCollectionRequestModel { [Required] [EncryptedString] @@ -40,7 +40,7 @@ public class CollectionBulkDeleteRequestModel public IEnumerable Ids { get; set; } } -public class CollectionWithIdRequestModel : CollectionRequestModel +public class CollectionWithIdRequestModel : CreateCollectionRequestModel { public Guid? Id { get; set; } @@ -50,3 +50,21 @@ public class CollectionWithIdRequestModel : CollectionRequestModel return base.ToCollection(existingCollection); } } + +public class UpdateCollectionRequestModel : CreateCollectionRequestModel +{ + [EncryptedString] + [EncryptedStringLength(1000)] + public new string Name { get; set; } + + public override Collection ToCollection(Collection existingCollection) + { + if (string.IsNullOrEmpty(existingCollection.DefaultUserCollectionEmail) && !string.IsNullOrWhiteSpace(Name)) + { + existingCollection.Name = Name; + } + existingCollection.ExternalId = ExternalId; + return existingCollection; + } + +} diff --git a/src/Api/Models/Response/CollectionResponseModel.cs b/src/Api/Models/Response/CollectionResponseModel.cs index d679250f05..10d56481c4 100644 --- a/src/Api/Models/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Response/CollectionResponseModel.cs @@ -49,6 +49,7 @@ public class CollectionDetailsResponseModel : CollectionResponseModel ReadOnly = collectionDetails.ReadOnly; HidePasswords = collectionDetails.HidePasswords; Manage = collectionDetails.Manage; + DefaultUserCollectionEmail = collectionDetails.DefaultUserCollectionEmail; } public bool ReadOnly { get; set; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index c6dd621c28..fae0598c1c 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -5,6 +5,7 @@ using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -73,53 +74,91 @@ public class OrganizationUserRepository : Repository u.Id).ToList(); } - public override async Task DeleteAsync(Core.Entities.OrganizationUser organizationUser) => await DeleteAsync(organizationUser.Id); - public async Task DeleteAsync(Guid organizationUserId) + public override async Task DeleteAsync(Core.Entities.OrganizationUser organizationUser) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserId); var orgUser = await dbContext.OrganizationUsers - .Where(ou => ou.Id == organizationUserId) - .FirstAsync(); + .Where(ou => ou.Id == organizationUser.Id) + .Select(ou => new + { + ou.Id, + ou.UserId, + OrgEmail = ou.Email, + UserEmail = ou.User.Email + }) + .FirstOrDefaultAsync(); - var organizationId = orgUser?.OrganizationId; + if (orgUser == null) + { + throw new NotFoundException("User not found."); + } + + var email = !string.IsNullOrEmpty(orgUser.OrgEmail) + ? orgUser.OrgEmail + : orgUser.UserEmail; + var organizationId = organizationUser?.OrganizationId; var userId = orgUser?.UserId; + var utcNow = DateTime.UtcNow; - if (orgUser?.OrganizationId != null && orgUser?.UserId != null) + using var transaction = await dbContext.Database.BeginTransactionAsync(); + + try { - var ssoUsers = dbContext.SsoUsers - .Where(su => su.UserId == userId && su.OrganizationId == organizationId); - dbContext.SsoUsers.RemoveRange(ssoUsers); + await dbContext.Collections + .Where(c => c.Type == CollectionType.DefaultUserCollection + && c.CollectionUsers.Any(cu => cu.OrganizationUserId == organizationUser.Id)) + .ExecuteUpdateAsync(setters => setters + .SetProperty(c => c.Type, CollectionType.SharedCollection) + .SetProperty(c => c.RevisionDate, utcNow) + .SetProperty(c => c.DefaultUserCollectionEmail, + c => c.DefaultUserCollectionEmail == null ? email : c.DefaultUserCollectionEmail)); + + await dbContext.CollectionUsers + .Where(cu => cu.OrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.GroupUsers + .Where(gu => gu.OrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.SsoUsers + .Where(su => su.UserId == userId && su.OrganizationId == organizationId) + .ExecuteDeleteAsync(); + + await dbContext.UserProjectAccessPolicy + .Where(ap => ap.OrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.UserServiceAccountAccessPolicy + .Where(ap => ap.OrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.UserSecretAccessPolicy + .Where(ap => ap.OrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.OrganizationSponsorships + .Where(os => os.SponsoringOrganizationUserId == organizationUser.Id) + .ExecuteDeleteAsync(); + + await dbContext.Users + .Where(u => u.Id == orgUser.UserId) + .ExecuteUpdateAsync(setters => setters + .SetProperty(u => u.AccountRevisionDate, utcNow)); + + await dbContext.OrganizationUsers + .Where(ou => ou.Id == organizationUser.Id) + .ExecuteDeleteAsync(); + + await transaction.CommitAsync(); } - - var collectionUsers = dbContext.CollectionUsers - .Where(cu => cu.OrganizationUserId == organizationUserId); - dbContext.CollectionUsers.RemoveRange(collectionUsers); - - var groupUsers = dbContext.GroupUsers - .Where(gu => gu.OrganizationUserId == organizationUserId); - dbContext.GroupUsers.RemoveRange(groupUsers); - - dbContext.UserProjectAccessPolicy.RemoveRange( - dbContext.UserProjectAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId)); - dbContext.UserServiceAccountAccessPolicy.RemoveRange( - dbContext.UserServiceAccountAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId)); - dbContext.UserSecretAccessPolicy.RemoveRange( - dbContext.UserSecretAccessPolicy.Where(ap => ap.OrganizationUserId == organizationUserId)); - - var orgSponsorships = await dbContext.OrganizationSponsorships - .Where(os => os.SponsoringOrganizationUserId == organizationUserId) - .ToListAsync(); - - foreach (var orgSponsorship in orgSponsorships) + catch { - orgSponsorship.ToDelete = true; + await transaction.RollbackAsync(); + throw; } - - dbContext.OrganizationUsers.Remove(orgUser); - await dbContext.SaveChangesAsync(); } } @@ -130,31 +169,92 @@ public class OrganizationUserRepository : Repository targetOrganizationUserIds.Contains(cu.OrganizationUserId)) - .ExecuteDeleteAsync(); + try + { + await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdsAsync(targetOrganizationUserIds); - await dbContext.GroupUsers - .Where(gu => targetOrganizationUserIds.Contains(gu.OrganizationUserId)) - .ExecuteDeleteAsync(); + var organizationUsersToDelete = await dbContext.OrganizationUsers + .Where(ou => targetOrganizationUserIds.Contains(ou.Id)) + .Include(ou => ou.User) + .ToListAsync(); - await dbContext.UserProjectAccessPolicy - .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) - .ExecuteDeleteAsync(); - await dbContext.UserServiceAccountAccessPolicy - .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) - .ExecuteDeleteAsync(); - await dbContext.UserSecretAccessPolicy - .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) - .ExecuteDeleteAsync(); + var collectionUsers = await dbContext.CollectionUsers + .Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId)) + .ToListAsync(); - await dbContext.OrganizationUsers - .Where(ou => targetOrganizationUserIds.Contains(ou.Id)).ExecuteDeleteAsync(); + var collectionIds = collectionUsers.Select(cu => cu.CollectionId).Distinct().ToList(); - await dbContext.SaveChangesAsync(); - await transaction.CommitAsync(); + var collections = await dbContext.Collections + .Where(c => collectionIds.Contains(c.Id)) + .ToListAsync(); + + var collectionsToUpdate = collections + .Where(c => c.Type == CollectionType.DefaultUserCollection) + .ToList(); + + var collectionUserLookup = collectionUsers.ToLookup(cu => cu.CollectionId); + + foreach (var collection in collectionsToUpdate) + { + var collectionUser = collectionUserLookup[collection.Id].FirstOrDefault(); + if (collectionUser != null) + { + var orgUser = organizationUsersToDelete.FirstOrDefault(ou => ou.Id == collectionUser.OrganizationUserId); + + if (orgUser?.User != null) + { + if (string.IsNullOrEmpty(collection.DefaultUserCollectionEmail)) + { + var emailToUse = !string.IsNullOrEmpty(orgUser.Email) + ? orgUser.Email + : orgUser.User.Email; + + if (!string.IsNullOrEmpty(emailToUse)) + { + collection.DefaultUserCollectionEmail = emailToUse; + } + } + collection.Type = CollectionType.SharedCollection; + } + } + } + + await dbContext.CollectionUsers + .Where(cu => targetOrganizationUserIds.Contains(cu.OrganizationUserId)) + .ExecuteDeleteAsync(); + + await dbContext.GroupUsers + .Where(gu => targetOrganizationUserIds.Contains(gu.OrganizationUserId)) + .ExecuteDeleteAsync(); + + await dbContext.UserProjectAccessPolicy + .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) + .ExecuteDeleteAsync(); + + await dbContext.UserServiceAccountAccessPolicy + .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) + .ExecuteDeleteAsync(); + + await dbContext.UserSecretAccessPolicy + .Where(ap => targetOrganizationUserIds.Contains(ap.OrganizationUserId!.Value)) + .ExecuteDeleteAsync(); + + await dbContext.OrganizationSponsorships + .Where(os => targetOrganizationUserIds.Contains(os.SponsoringOrganizationUserId)) + .ExecuteDeleteAsync(); + + await dbContext.OrganizationUsers + .Where(ou => targetOrganizationUserIds.Contains(ou.Id)).ExecuteDeleteAsync(); + + await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + } + catch + { + await transaction.RollbackAsync(); + throw; + } } public async Task>> GetByIdWithCollectionsAsync(Guid id) diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 9f047e4653..569e541163 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -325,7 +325,8 @@ public class CollectionRepository : Repository new CollectionAdminDetails { Id = collectionGroup.Key.Id, @@ -339,7 +340,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), - Unmanaged = collectionGroup.Key.Unmanaged + Unmanaged = collectionGroup.Key.Unmanaged, + DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail }).ToList(); } else @@ -353,7 +355,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), - Unmanaged = collectionGroup.Key.Unmanaged + Unmanaged = collectionGroup.Key.Unmanaged, + DefaultUserCollectionEmail = collectionGroup.Key.DefaultUserCollectionEmail }).ToListAsync(); } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs index c893bff15c..2b6e61d056 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs @@ -81,6 +81,7 @@ public class CollectionAdminDetailsQuery : IQuery ExternalId = x.c.ExternalId, CreationDate = x.c.CreationDate, RevisionDate = x.c.RevisionDate, + DefaultUserCollectionEmail = x.c.DefaultUserCollectionEmail, ReadOnly = (bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false, HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false, Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false, diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql index d706bd4d75..fc95cb112a 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById] +CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById] @Id UNIQUEIDENTIFIER AS BEGIN @@ -17,6 +17,11 @@ BEGIN WHERE [Id] = @Id + -- Migrate DefaultUserCollection to SharedCollection + DECLARE @Ids [dbo].[GuidIdArray] + INSERT INTO @Ids (Id) VALUES (@Id) + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids + IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL BEGIN EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql index ac9e75dd5e..79e060c323 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql @@ -6,6 +6,9 @@ BEGIN EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids + -- Migrate DefaultCollection to SharedCollection + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids + DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray] INSERT INTO @UserAndOrganizationIds diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_MigrateDefaultCollection.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_MigrateDefaultCollection.sql new file mode 100644 index 0000000000..f65cdc3983 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_MigrateDefaultCollection.sql @@ -0,0 +1,22 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_MigrateDefaultCollection] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE c + SET + [DefaultUserCollectionEmail] = CASE WHEN c.[DefaultUserCollectionEmail] IS NULL THEN u.[Email] ELSE c.[DefaultUserCollectionEmail] END, + [RevisionDate] = @UtcNow, + [Type] = 0 + FROM + [dbo].[Collection] c + INNER JOIN [dbo].[CollectionUser] cu ON c.[Id] = cu.[CollectionId] + INNER JOIN [dbo].[OrganizationUser] ou ON cu.[OrganizationUserId] = ou.[Id] + INNER JOIN [dbo].[User] u ON ou.[UserId] = u.[Id] + INNER JOIN @Ids i ON ou.[Id] = i.[Id] + WHERE + c.[Type] = 1 +END +GO diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 99e329b500..a3d34efb63 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -22,7 +22,7 @@ namespace Bit.Api.Test.Controllers; public class CollectionsControllerTests { [Theory, BitAutoData] - public async Task Post_Success(Organization organization, CollectionRequestModel collectionRequest, + public async Task Post_Success(Organization organization, CreateCollectionRequestModel collectionRequest, SutProvider sutProvider) { Collection ExpectedCollection() => Arg.Is(c => @@ -46,9 +46,10 @@ public class CollectionsControllerTests } [Theory, BitAutoData] - public async Task Put_Success(Collection collection, CollectionRequestModel collectionRequest, + public async Task Put_Success(Collection collection, UpdateCollectionRequestModel collectionRequest, SutProvider sutProvider) { + collection.DefaultUserCollectionEmail = null; Collection ExpectedCollection() => Arg.Is(c => c.Id == collection.Id && c.Name == collectionRequest.Name && c.ExternalId == collectionRequest.ExternalId && c.OrganizationId == collection.OrganizationId); @@ -72,7 +73,7 @@ public class CollectionsControllerTests } [Theory, BitAutoData] - public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, CollectionRequestModel collectionRequest, + public async Task Put_WithNoCollectionPermission_ThrowsNotFound(Collection collection, UpdateCollectionRequestModel collectionRequest, SutProvider sutProvider) { sutProvider.GetDependency() @@ -484,4 +485,176 @@ public class CollectionsControllerTests await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .AddAccessAsync(default, default, default); } + + [Theory, BitAutoData] + public async Task Put_With_NonNullName_DoesNotPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + // Arrange + var newName = "new name"; + var originalName = "original name"; + + existingCollection.Name = originalName; + existingCollection.DefaultUserCollectionEmail = null; + + collectionRequest.Name = newName; + + sutProvider.GetDependency() + .GetByIdAsync(existingCollection.Id) + .Returns(existingCollection); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + existingCollection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + // Act + await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync( + Arg.Is(c => c.Id == existingCollection.Id && c.Name == newName), + Arg.Any>(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task Put_WithNullName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + // Arrange + var originalName = "original name"; + + existingCollection.Name = originalName; + existingCollection.DefaultUserCollectionEmail = null; + + collectionRequest.Name = null; + + sutProvider.GetDependency() + .GetByIdAsync(existingCollection.Id) + .Returns(existingCollection); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + existingCollection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + // Act + await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync( + Arg.Is(c => c.Id == existingCollection.Id && c.Name == originalName), + Arg.Any>(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task Put_WithDefaultUserCollectionEmail_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + // Arrange + var originalName = "original name"; + var defaultUserCollectionEmail = "user@email.com"; + + existingCollection.Name = originalName; + existingCollection.DefaultUserCollectionEmail = defaultUserCollectionEmail; + + collectionRequest.Name = "new name"; + + sutProvider.GetDependency() + .GetByIdAsync(existingCollection.Id) + .Returns(existingCollection); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + existingCollection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + // Act + await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync( + Arg.Is(c => c.Id == existingCollection.Id && c.Name == originalName && c.DefaultUserCollectionEmail == defaultUserCollectionEmail), + Arg.Any>(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task Put_WithEmptyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + // Arrange + var originalName = "original name"; + + existingCollection.Name = originalName; + existingCollection.DefaultUserCollectionEmail = null; + + collectionRequest.Name = ""; // Empty string + + sutProvider.GetDependency() + .GetByIdAsync(existingCollection.Id) + .Returns(existingCollection); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + existingCollection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + // Act + await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync( + Arg.Is(c => c.Id == existingCollection.Id && c.Name == originalName), + Arg.Any>(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task Put_WithWhitespaceOnlyName_DoesPreserveExistingName(Collection existingCollection, UpdateCollectionRequestModel collectionRequest, + SutProvider sutProvider) + { + // Arrange + var originalName = "original name"; + + existingCollection.Name = originalName; + existingCollection.DefaultUserCollectionEmail = null; + + collectionRequest.Name = " "; // Whitespace only + + sutProvider.GetDependency() + .GetByIdAsync(existingCollection.Id) + .Returns(existingCollection); + + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), + existingCollection, + Arg.Is>(r => r.Contains(BulkCollectionOperations.Update))) + .Returns(AuthorizationResult.Success()); + + // Act + await sutProvider.Sut.Put(existingCollection.OrganizationId, existingCollection.Id, collectionRequest); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpdateAsync( + Arg.Is(c => c.Id == existingCollection.Id && c.Name == originalName), + Arg.Any>(), + Arg.Any>()); + } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs index ae30fb4bed..67e2c1910b 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -57,8 +57,8 @@ public class OrganizationRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = $"Test Org {id}", - BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL PrivateKey = "privatekey", }); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 612e8d1074..a07d5c934b 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -28,8 +28,8 @@ public class OrganizationUserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = user.Email, // TODO: EF does not enfore this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL }); var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser @@ -37,6 +37,7 @@ public class OrganizationUserRepositoryTests OrganizationId = organization.Id, UserId = user.Id, Status = OrganizationUserStatusType.Confirmed, + Email = user.Email }); await organizationUserRepository.DeleteAsync(orgUser); @@ -46,6 +47,171 @@ public class OrganizationUserRepositoryTests Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate); } + [DatabaseTheory, DatabaseData] + public async Task DeleteManyAsync_Migrates_UserDefaultCollection(IUserRepository userRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository + ) + { + var user1 = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var user2 = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL + }); + + var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Email = user1.Email + }); + + var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user2.Id, + Status = OrganizationUserStatusType.Confirmed, + Email = user2.Email + }); + + var defaultUserCollection1 = await collectionRepository.CreateAsync(new Collection + { + Name = "Test Collection 1", + Id = user1.Id, + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }); + + var defaultUserCollection2 = await collectionRepository.CreateAsync(new Collection + { + Name = "Test Collection 2", + Id = user2.Id, + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }); + + // Create the CollectionUser entry for the defaultUserCollection + await collectionRepository.UpdateUsersAsync(defaultUserCollection1.Id, new List() + { + new CollectionAccessSelection + { + Id = orgUser1.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await collectionRepository.UpdateUsersAsync(defaultUserCollection2.Id, new List() + { + new CollectionAccessSelection + { + Id = orgUser2.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await organizationUserRepository.DeleteManyAsync(new List { orgUser1.Id, orgUser2.Id }); + + var newUser = await userRepository.GetByIdAsync(user1.Id); + Assert.NotNull(newUser); + Assert.NotEqual(newUser.AccountRevisionDate, user1.AccountRevisionDate); + + var updatedCollection1 = await collectionRepository.GetByIdAsync(defaultUserCollection1.Id); + Assert.NotNull(updatedCollection1); + Assert.Equal(CollectionType.SharedCollection, updatedCollection1.Type); + Assert.Equal(user1.Email, updatedCollection1.DefaultUserCollectionEmail); + + var updatedCollection2 = await collectionRepository.GetByIdAsync(defaultUserCollection2.Id); + Assert.NotNull(updatedCollection2); + Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type); + Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail); + } + + [DatabaseTheory, DatabaseData] + public async Task DeleteAsync_Migrates_UserDefaultCollection(IUserRepository userRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository + ) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Email = user.Email + }); + + var defaultUserCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Test Collection", + Id = user.Id, + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }); + + // Create the CollectionUser entry for the defaultUserCollection + await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List() + { + new CollectionAccessSelection + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await organizationUserRepository.DeleteAsync(orgUser); + + var newUser = await userRepository.GetByIdAsync(user.Id); + Assert.NotNull(newUser); + Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate); + + var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id); + Assert.NotNull(updatedCollection); + Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type); + Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail); + } + + [DatabaseTheory, DatabaseData] public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -70,8 +236,8 @@ public class OrganizationUserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL }); var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser @@ -79,6 +245,7 @@ public class OrganizationUserRepositoryTests OrganizationId = organization.Id, UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, + Email = user1.Email }); var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser @@ -86,6 +253,7 @@ public class OrganizationUserRepositoryTests OrganizationId = organization.Id, UserId = user2.Id, Status = OrganizationUserStatusType.Confirmed, + Email = user2.Email }); await organizationUserRepository.DeleteManyAsync(new List @@ -135,8 +303,8 @@ public class OrganizationUserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL PrivateKey = "privatekey", }); @@ -291,8 +459,8 @@ public class OrganizationUserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL PrivateKey = "privatekey", }); @@ -354,6 +522,134 @@ public class OrganizationUserRepositoryTests Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies); } + [DatabaseTheory, DatabaseData] + public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationDomainRepository organizationDomainRepository) + { + var id = Guid.NewGuid(); + var domainName = $"{id}.example.com"; + + var user1 = await userRepository.CreateAsync(new User + { + Name = "Test User 1", + Email = $"test+{id}@{domainName}", + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var user2 = await userRepository.CreateAsync(new User + { + Name = "Test User 2", + Email = $"test+{id}@x-{domainName}", // Different domain + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var user3 = await userRepository.CreateAsync(new User + { + Name = "Test User 2", + Email = $"test+{id}@{domainName}.example.com", // Different domain + ApiKey = "TEST", + SecurityStamp = "stamp", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 1, + KdfMemory = 2, + KdfParallelism = 3 + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = $"Test Org {id}", + BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL + PrivateKey = "privatekey", + UsePolicies = false, + UseSso = false, + UseKeyConnector = false, + UseScim = false, + UseGroups = false, + UseDirectory = false, + UseEvents = false, + UseTotp = false, + Use2fa = false, + UseApi = false, + UseResetPassword = false, + UseSecretsManager = false, + SelfHost = false, + UsersGetPremium = false, + UseCustomPermissions = false, + Enabled = true, + UsePasswordManager = false, + LimitCollectionCreation = false, + LimitCollectionDeletion = false, + LimitItemDeletion = false, + AllowAdminAccessToAllCollectionItems = false, + UseRiskInsights = false, + UseAdminSponsoredFamilies = false + }); + + var organizationDomain = new OrganizationDomain + { + OrganizationId = organization.Id, + DomainName = domainName, + Txt = "btw+12345", + }; + organizationDomain.SetVerifiedDate(); + organizationDomain.SetNextRunDate(12); + organizationDomain.SetJobRunCount(); + await organizationDomainRepository.CreateAsync(organizationDomain); + + var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user1.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user2.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false + }); + + await organizationUserRepository.CreateAsync(new OrganizationUser + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + UserId = user3.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User, + ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false + }); + + var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); + + Assert.NotNull(responseModel); + Assert.Single(responseModel); + Assert.Equal(orgUser1.Id, responseModel.Single().Id); + } + [DatabaseTheory, DatabaseData] public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository, IUserRepository userRepository, @@ -369,7 +665,7 @@ public class OrganizationUserRepositoryTests { Name = $"test-{Guid.NewGuid()}", BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + Plan = "Test", // TODO: EF does not enforce this being NOT NULL }); var orgUsers = users.Select(u => new OrganizationUser @@ -403,7 +699,7 @@ public class OrganizationUserRepositoryTests { Name = $"test-{Guid.NewGuid()}", BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + Plan = "Test", // TODO: EF does not enforce this being NOT NULL }); var orgUsers = users.Select(u => new OrganizationUser @@ -435,8 +731,8 @@ public class OrganizationUserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = "billing@test.com", // TODO: EF does not enfore this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl, + BillingEmail = "billing@test.com", // TODO: EF does not enforce this being NOT NULL + Plan = "Test", // TODO: EF does not enforce this being NOT NULL, CreationDate = requestTime }); @@ -862,119 +1158,6 @@ public class OrganizationUserRepositoryTests Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id); } - [DatabaseTheory, DatabaseData] - public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationDomainRepository organizationDomainRepository) - { - var id = Guid.NewGuid(); - var domainName = $"{id}.example.com"; - var requestTime = DateTime.UtcNow; - - var user1 = await userRepository.CreateAsync(new User - { - Id = CoreHelpers.GenerateComb(), - Name = "Test User 1", - Email = $"test+{id}@{domainName}", - ApiKey = "TEST", - SecurityStamp = "stamp", - CreationDate = requestTime, - RevisionDate = requestTime, - AccountRevisionDate = requestTime - }); - - var user2 = await userRepository.CreateAsync(new User - { - Id = CoreHelpers.GenerateComb(), - Name = "Test User 2", - Email = $"test+{id}@x-{domainName}", // Different domain - ApiKey = "TEST", - SecurityStamp = "stamp", - CreationDate = requestTime, - RevisionDate = requestTime, - AccountRevisionDate = requestTime - }); - - var user3 = await userRepository.CreateAsync(new User - { - Id = CoreHelpers.GenerateComb(), - Name = "Test User 3", - Email = $"test+{id}@{domainName}.example.com", // Different domain - ApiKey = "TEST", - SecurityStamp = "stamp", - CreationDate = requestTime, - RevisionDate = requestTime, - AccountRevisionDate = requestTime - }); - - var organization = await organizationRepository.CreateAsync(new Organization - { - Id = CoreHelpers.GenerateComb(), - Name = $"Test Org {id}", - BillingEmail = user1.Email, - Plan = "Test", - Enabled = true, - CreationDate = requestTime, - RevisionDate = requestTime - }); - - var organizationDomain = new OrganizationDomain - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - DomainName = domainName, - Txt = "btw+12345", - CreationDate = requestTime - }; - organizationDomain.SetNextRunDate(12); - organizationDomain.SetVerifiedDate(); - organizationDomain.SetJobRunCount(); - await organizationDomainRepository.CreateAsync(organizationDomain); - - var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user1.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Owner, - CreationDate = requestTime, - RevisionDate = requestTime - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user2.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - CreationDate = requestTime, - RevisionDate = requestTime - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser - { - Id = CoreHelpers.GenerateComb(), - OrganizationId = organization.Id, - UserId = user3.Id, - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - CreationDate = requestTime, - RevisionDate = requestTime - }); - - var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id); - - Assert.NotNull(responseModel); - Assert.Single(responseModel); - Assert.Equal(orgUser1.Id, responseModel.Single().Id); - Assert.Equal(user1.Id, responseModel.Single().UserId); - Assert.Equal(organization.Id, responseModel.Single().OrganizationId); - } - [DatabaseTheory, DatabaseData] public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty( IUserRepository userRepository, @@ -1039,6 +1222,120 @@ public class OrganizationUserRepositoryTests Assert.Empty(responseModel); } + [DatabaseTheory, DatabaseData] + public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository + ) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user.Email, + Plan = "Test", + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Email = null + }); + + var defaultUserCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Test Collection", + Id = user.Id, + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }); + + await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List() + { + new CollectionAccessSelection + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await organizationUserRepository.DeleteAsync(orgUser); + + var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id); + Assert.NotNull(updatedCollection); + Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type); + Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail); + } + + [DatabaseTheory, DatabaseData] + public async Task DeleteAsync_WithEmptyEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository + ) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user.Email, + Plan = "Test", + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Email = "" // Empty string email + }); + + var defaultUserCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Test Collection", + Id = user.Id, + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }); + + await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List() + { + new CollectionAccessSelection + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }, + }); + + await organizationUserRepository.DeleteAsync(orgUser); + + var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id); + Assert.NotNull(updatedCollection); + Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type); + Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail); + } + [DatabaseTheory, DatabaseData] public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess( IUserRepository userRepository, diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs index d4606ae632..0bf0909a0a 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs @@ -55,7 +55,7 @@ public class UserRepositoryTests var organization = await organizationRepository.CreateAsync(new Organization { Name = "Test Org", - BillingEmail = user3.Email, // TODO: EF does not enfore this being NOT NULL + BillingEmail = user3.Email, // TODO: EF does not enforce this being NOT NULL Plan = "Test", // TODO: EF does not enforce this being NOT NULl }); diff --git a/util/Migrator/DbScripts/2025-08-04-00_OrgUsers_MigrateDefaultCollection.sql b/util/Migrator/DbScripts/2025-08-04-00_OrgUsers_MigrateDefaultCollection.sql new file mode 100644 index 0000000000..5ad83967e0 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-04-00_OrgUsers_MigrateDefaultCollection.sql @@ -0,0 +1,22 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_MigrateDefaultCollection] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + DECLARE @UtcNow DATETIME2(7) = GETUTCDATE(); + + UPDATE c + SET + [DefaultUserCollectionEmail] = CASE WHEN c.[DefaultUserCollectionEmail] IS NULL THEN u.[Email] ELSE c.[DefaultUserCollectionEmail] END, + [RevisionDate] = @UtcNow, + [Type] = 0 + FROM + [dbo].[Collection] c + INNER JOIN [dbo].[CollectionUser] cu ON c.[Id] = cu.[CollectionId] + INNER JOIN [dbo].[OrganizationUser] ou ON cu.[OrganizationUserId] = ou.[Id] + INNER JOIN [dbo].[User] u ON ou.[UserId] = u.[Id] + INNER JOIN @Ids i ON ou.[Id] = i.[Id] + WHERE + c.[Type] = 1 +END +GO diff --git a/util/Migrator/DbScripts/2025-08-04-01_OrgUsers_DeleteById.sql b/util/Migrator/DbScripts/2025-08-04-01_OrgUsers_DeleteById.sql new file mode 100644 index 0000000000..b8447764a0 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-04-01_OrgUsers_DeleteById.sql @@ -0,0 +1,55 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id + + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @UserId UNIQUEIDENTIFIER + + SELECT + @OrganizationId = [OrganizationId], + @UserId = [UserId] + FROM + [dbo].[OrganizationUser] + WHERE + [Id] = @Id + + -- Migrate DefaultUserCollection to SharedCollection + DECLARE @Ids [dbo].[GuidIdArray] + INSERT INTO @Ids (Id) VALUES (@Id) + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids + + IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL + BEGIN + EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId + END + + DELETE + FROM + [dbo].[CollectionUser] + WHERE + [OrganizationUserId] = @Id + + DELETE + FROM + [dbo].[GroupUser] + WHERE + [OrganizationUserId] = @Id + + DELETE + FROM + [dbo].[AccessPolicy] + WHERE + [OrganizationUserId] = @Id + + EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [Id] = @Id +END diff --git a/util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql b/util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql new file mode 100644 index 0000000000..9352416d30 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-04-02_OrgUsers_DeleteByIds.sql @@ -0,0 +1,105 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_DeleteByIds] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids + + -- Migrate DefaultCollection to SharedCollection + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @Ids + + DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray] + + INSERT INTO @UserAndOrganizationIds + (Id1, Id2) + SELECT + UserId, + OrganizationId + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @Ids OUIds ON OUIds.Id = OU.Id + WHERE + UserId IS NOT NULL AND + OrganizationId IS NOT NULL + + BEGIN + EXEC [dbo].[SsoUser_DeleteMany] @UserAndOrganizationIds + END + + DECLARE @BatchSize INT = 100 + + -- Delete CollectionUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION CollectionUser_DeleteMany_CUs + + DELETE TOP(@BatchSize) CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + @Ids I ON I.Id = CU.OrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION CollectionUser_DeleteMany_CUs + END + + SET @BatchSize = 100; + + -- Delete GroupUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION GroupUser_DeleteMany_GroupUsers + + DELETE TOP(@BatchSize) GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + @Ids I ON I.Id = GU.OrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION GroupUser_DeleteMany_GroupUsers + END + + SET @BatchSize = 100; + + -- Delete User Access Policies + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION AccessPolicy_DeleteMany_Users + + DELETE TOP(@BatchSize) AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + @Ids I ON I.Id = AP.OrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION AccessPolicy_DeleteMany_Users + END + + EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids + + SET @BatchSize = 100; + + -- Delete OrganizationUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrganizationUser_DeleteMany_OUs + + DELETE TOP(@BatchSize) OU + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @Ids I ON I.Id = OU.Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrganizationUser_DeleteMany_OUs + END +END +GO From 3169c5fb85cec0e5a077285b5a0233cfd3729e2d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 12:19:12 +0200 Subject: [PATCH 150/326] [deps]: Update github-action minor (#5865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Amy Galles <9685081+AmyLGalles@users.noreply.github.com> Co-authored-by: Daniel García --- .github/workflows/_move_edd_db_scripts.yml | 2 +- .github/workflows/build.yml | 20 ++++++++++---------- .github/workflows/release.yml | 2 +- .github/workflows/repository-management.yml | 4 ++-- .github/workflows/test-database.yml | 8 ++++---- .github/workflows/test.yml | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/_move_edd_db_scripts.yml b/.github/workflows/_move_edd_db_scripts.yml index 98fe4f1f05..b38a3e0dff 100644 --- a/.github/workflows/_move_edd_db_scripts.yml +++ b/.github/workflows/_move_edd_db_scripts.yml @@ -153,7 +153,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Import GPG keys - uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0 + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 with: gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d08145b5a..7de7798a39 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@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Verify format run: dotnet format --verify-no-changes @@ -117,10 +117,10 @@ jobs: fi - name: Set up .NET - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Set up Node - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: cache: "npm" cache-dependency-path: "**/package-lock.json" @@ -166,10 +166,10 @@ jobs: ########## Set up Docker ########## - name: Set up QEMU emulators - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 ########## ACRs ########## - name: Log in to Azure @@ -237,7 +237,7 @@ jobs: - name: Build Docker image id: build-artifacts - uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -252,7 +252,7 @@ jobs: - name: Install Cosign if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 + uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2 - name: Sign image with Cosign if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' @@ -269,7 +269,7 @@ jobs: - name: Scan Docker image id: container-scan - uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0 + uses: anchore/scan-action@2c901ab7378897c01b8efaa2d0c9bf519cc64b9e # v6.2.0 with: image: ${{ steps.image-tags.outputs.primary_tag }} fail-build: false @@ -299,7 +299,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Set up .NET - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - 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@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Print environment run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c62587fe39..8bb19b4da1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -86,7 +86,7 @@ jobs: - name: Create release if: ${{ inputs.release_type != 'Dry Run' }} - uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0 + uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 with: artifacts: "docker-stub-US.zip, docker-stub-EU.zip, diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index b5d6db69d4..18192ca0ad 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -82,7 +82,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Generate GH App token - uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} @@ -200,7 +200,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token - uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index 65417f7529..6bbc33299f 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@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Restore tools run: dotnet tool restore @@ -154,7 +154,7 @@ jobs: run: 'docker logs $(docker ps --quiet --filter "name=mssql")' - name: Report test results - uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0 + uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results @@ -163,7 +163,7 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 - name: Docker Compose down if: always() @@ -179,7 +179,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up .NET - uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Print environment run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e44d7aa8b8..4eed6df7ab 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@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 - name: Print environment run: | @@ -49,7 +49,7 @@ jobs: run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" - name: Report test results - uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0 + uses: dorny/test-reporter@890a17cecf52a379fc869ab770a71657660be727 # v2.1.0 if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }} with: name: Test Results @@ -58,4 +58,4 @@ jobs: fail-on-error: true - name: Upload to codecov.io - uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 From 9face764178495377ddc19591b61522bca08339f Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 20 Aug 2025 09:27:05 -0400 Subject: [PATCH 151/326] [PM-22980] Organization name not updated in Stripe when organization name is changed (#6189) * tests: add tests for UpdateAsync change * fix: update Stripe customer object update * refactor: replace CustomerService objects with stripeAdapter * refactor: simplify controller logic * fix: mark businessname and it's function obsolete for future use * fix: pr feedback remove business name check * refactor: remove unused functions in organizationservice --- .../Controllers/OrganizationsController.cs | 11 +- .../AdminConsole/Entities/Organization.cs | 3 + .../Services/IOrganizationService.cs | 1 - .../Implementations/OrganizationService.cs | 45 +++---- .../Services/OrganizationServiceTests.cs | 126 ++++++++++++++++++ 5 files changed, 159 insertions(+), 27 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 18045178db..8b1a6243c3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -12,6 +12,7 @@ using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; @@ -235,8 +236,7 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() || - model.BillingEmail != organization.BillingEmail); + var updateBilling = ShouldUpdateBilling(model, organization); var hasRequiredPermissions = updateBilling ? await _currentContext.EditSubscription(orgIdGuid) @@ -582,4 +582,11 @@ public class OrganizationsController : Controller return organization.PlanType; } + + private bool ShouldUpdateBilling(OrganizationUpdateRequestModel model, Organization organization) + { + var organizationNameChanged = model.Name != organization.Name; + var billingEmailChanged = model.BillingEmail != organization.BillingEmail; + return !_globalSettings.SelfHosted && (organizationNameChanged || billingEmailChanged); + } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 3f02462501..7933990e74 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -30,6 +30,7 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable /// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead. /// [MaxLength(50)] + [Obsolete("This property has been deprecated. Use the 'Name' property instead.")] public string? BusinessName { get; set; } [MaxLength(50)] public string? BusinessAddress1 { get; set; } @@ -147,6 +148,8 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable /// /// Returns the business name of the organization, HTML decoded ready for display. /// + /// + [Obsolete("This method has been deprecated. Use the 'DisplayName()' method instead.")] public string? DisplayBusinessName() { return WebUtility.HtmlDecode(BusinessName); diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 6adfc4772f..e54e6fee12 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -13,7 +13,6 @@ namespace Bit.Core.Services; public interface IOrganizationService { - Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task ReinstateSubscriptionAsync(Guid organizationId); Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 41e4f2f618..575cdb0230 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -65,6 +65,7 @@ public class OrganizationService : IOrganizationService private readonly IPricingClient _pricingClient; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; + private readonly IStripeAdapter _stripeAdapter; public OrganizationService( IOrganizationRepository organizationRepository, @@ -90,7 +91,8 @@ public class OrganizationService : IOrganizationService IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, - ISendOrganizationInvitesCommand sendOrganizationInvitesCommand + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, + IStripeAdapter stripeAdapter ) { _organizationRepository = organizationRepository; @@ -117,24 +119,7 @@ public class OrganizationService : IOrganizationService _pricingClient = pricingClient; _policyRequirementQuery = policyRequirementQuery; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; - } - - public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - var eop = endOfPeriod.GetValueOrDefault(true); - if (!endOfPeriod.HasValue && organization.ExpirationDate.HasValue && - organization.ExpirationDate.Value < DateTime.UtcNow) - { - eop = false; - } - - await _paymentService.CancelSubscriptionAsync(organization, eop); + _stripeAdapter = stripeAdapter; } public async Task ReinstateSubscriptionAsync(Guid organizationId) @@ -355,8 +340,7 @@ public class OrganizationService : IOrganizationService } var bankService = new BankAccountService(); - var customerService = new CustomerService(); - var customer = await customerService.GetAsync(organization.GatewayCustomerId, + var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, new CustomerGetOptions { Expand = new List { "sources" } }); if (customer == null) { @@ -417,12 +401,25 @@ public class OrganizationService : IOrganizationService if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) { - var customerService = new CustomerService(); - await customerService.UpdateAsync(organization.GatewayCustomerId, + var newDisplayName = organization.DisplayName(); + + await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions { Email = organization.BillingEmail, - Description = organization.DisplayBusinessName() + Description = organization.DisplayBusinessName(), + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + // This overwrites the existing custom fields for this organization + CustomFields = [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = organization.SubscriberType(), + Value = newDisplayName.Length <= 30 + ? newDisplayName + : newDisplayName[..30] + }] + }, }); } diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index 923eaae871..f619fed278 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -27,7 +27,9 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ExceptionExtensions; +using NSubstitute.ReceivedExtensions; using NSubstitute.ReturnsExtensions; +using Stripe; using Xunit; using Organization = Bit.Core.AdminConsole.Entities.Organization; using OrganizationUser = Bit.Core.Entities.OrganizationUser; @@ -1235,6 +1237,130 @@ public class OrganizationServiceTests await sutProvider.Sut.ValidateOrganizationCustomPermissionsEnabledAsync(organization.Id, OrganizationUserType.Custom); } + [Theory, BitAutoData] + public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsTrue_UpdateStripeCustomerAndOrganization(Organization organization, SutProvider sutProvider) + { + // Arrange + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + + var requestOptionsReturned = new CustomerUpdateOptions + { + Email = organization.BillingEmail, + Description = organization.DisplayBusinessName(), + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + // This overwrites the existing custom fields for this organization + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = organization.SubscriberType(), + Value = organization.DisplayName()[..30] + } + ] + }, + }; + organizationRepository + .GetByIdentifierAsync(organization.Identifier!) + .Returns(organization); + + // Act + await sutProvider.Sut.UpdateAsync(organization, updateBilling: true); + + // Assert + await organizationRepository + .Received(1) + .GetByIdentifierAsync(Arg.Is(id => id == organization.Identifier)); + await stripeAdapter + .Received(1) + .CustomerUpdateAsync( + Arg.Is(id => id == organization.GatewayCustomerId), + Arg.Is(options => options.Email == requestOptionsReturned.Email + && options.Description == requestOptionsReturned.Description + && options.InvoiceSettings.CustomFields.First().Name == requestOptionsReturned.InvoiceSettings.CustomFields.First().Name + && options.InvoiceSettings.CustomFields.First().Value == requestOptionsReturned.InvoiceSettings.CustomFields.First().Value)); ; + await organizationRepository + .Received(1) + .ReplaceAsync(Arg.Is(org => org == organization)); + await applicationCacheService + .Received(1) + .UpsertOrganizationAbilityAsync(Arg.Is(org => org == organization)); + await eventService + .Received(1) + .LogOrganizationEventAsync(Arg.Is(org => org == organization), + Arg.Is(e => e == EventType.Organization_Updated)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WhenValidOrganization_AndUpdateBillingIsFalse_UpdateOrganization(Organization organization, SutProvider sutProvider) + { + // Arrange + var organizationRepository = sutProvider.GetDependency(); + var applicationCacheService = sutProvider.GetDependency(); + var stripeAdapter = sutProvider.GetDependency(); + var eventService = sutProvider.GetDependency(); + + organizationRepository + .GetByIdentifierAsync(organization.Identifier!) + .Returns(organization); + + // Act + await sutProvider.Sut.UpdateAsync(organization, updateBilling: false); + + // Assert + await organizationRepository + .Received(1) + .GetByIdentifierAsync(Arg.Is(id => id == organization.Identifier)); + await stripeAdapter + .DidNotReceiveWithAnyArgs() + .CustomerUpdateAsync(Arg.Any(), Arg.Any()); + await organizationRepository + .Received(1) + .ReplaceAsync(Arg.Is(org => org == organization)); + await applicationCacheService + .Received(1) + .UpsertOrganizationAbilityAsync(Arg.Is(org => org == organization)); + await eventService + .Received(1) + .LogOrganizationEventAsync(Arg.Is(org => org == organization), + Arg.Is(e => e == EventType.Organization_Updated)); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WhenOrganizationHasNoId_ThrowsApplicationException(Organization organization, SutProvider sutProvider) + { + // Arrange + organization.Id = Guid.Empty; + + // Act/Assert + var exception = await Assert.ThrowsAnyAsync(() => sutProvider.Sut.UpdateAsync(organization)); + Assert.Equal("Cannot create org this way. Call SignUpAsync.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task UpdateAsync_WhenIdentifierAlreadyExistsForADifferentOrganization_ThrowsBadRequestException(Organization organization, SutProvider sutProvider) + { + // Arrange + var organizationRepository = sutProvider.GetDependency(); + var differentOrganization = new Organization { Id = Guid.NewGuid() }; + + organizationRepository + .GetByIdentifierAsync(organization.Identifier!) + .Returns(differentOrganization); + + // Act/Assert + var exception = await Assert.ThrowsAnyAsync(() => sutProvider.Sut.UpdateAsync(organization)); + Assert.Equal("Identifier already in use by another organization.", exception.Message); + + await organizationRepository + .Received(1) + .GetByIdentifierAsync(Arg.Is(id => id == organization.Identifier)); + } + // Must set real guids in order for dictionary of guids to not throw aggregate exceptions private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository) { From 7a6fa5a457c969d96575f9729229f1aa25b9bee6 Mon Sep 17 00:00:00 2001 From: Matt Andreko Date: Wed, 20 Aug 2025 09:39:11 -0400 Subject: [PATCH 152/326] Revert "Temporarily hold sarif uploads (#6166)" (#6222) --- .github/workflows/build.yml | 12 ++++++------ .github/workflows/scan.yml | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7de7798a39..30fcf29206 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,12 +275,12 @@ jobs: fail-build: false output-format: sarif -# - name: Upload Grype results to GitHub -# uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 -# with: -# sarif_file: ${{ steps.container-scan.outputs.sarif }} -# sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} -# ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} + - name: Upload Grype results to GitHub + uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8 + with: + sarif_file: ${{ steps.container-scan.outputs.sarif }} + sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }} + ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }} - name: Log out from Azure uses: bitwarden/gh-actions/azure-logout@main diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 04629ec899..f1d9370c29 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -38,8 +38,6 @@ jobs: pull-requests: write security-events: write id-token: write - with: - upload-sarif: false quality: name: Sonar From 3cad054af19f0b43afb3a64500a9cf7d8044b543 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 20 Aug 2025 10:24:17 -0400 Subject: [PATCH 153/326] [SM-1274] Adding Project Events (#6022) * Adding new logging for secrets * fixing secrest controller tests * fixing the tests * Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController * Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs * Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs * db changes * fixing the way we log events * Trying to undo some manual changes that should have been migrations * adding migration files * fixing test * setting up userid for project controller tests * adding sql * sql * Rename file * Trying to get it to for sure add the column before we try and update sprocs * Adding code to refresh the view to include ProjectId I hope * code improvements * Suggested changes * suggested changes * trying to fix sql issues * fixing swagger issue * Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Suggested changes --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../Repositories/ProjectRepository.cs | 5 +- .../Repositories/SecretRepository.cs | 19 +- .../Controllers/EventsController.cs | 126 +- .../Models/Response/EventResponseModel.cs | 2 + .../Models/Response/EventResponseModel.cs | 6 + .../Controllers/ProjectsController.cs | 42 +- .../Controllers/SecretsTrashController.cs | 36 +- src/Core/AdminConsole/Entities/Event.cs | 2 + src/Core/AdminConsole/Enums/EventType.cs | 7 + .../AdminConsole/Models/Data/EventMessage.cs | 1 + .../Models/Data/EventTableEntity.cs | 16 +- src/Core/AdminConsole/Models/Data/IEvent.cs | 1 + .../Repositories/IEventRepository.cs | 8 + .../TableStorage/EventRepository.cs | 15 + .../AdminConsole/Services/IEventService.cs | 2 + .../Services/Implementations/EventService.cs | 52 + .../NoopImplementations/NoopEventService.cs | 12 + .../Repositories/ISecretRepository.cs | 1 + .../Repositories/Noop/NoopSecretRepository.cs | 2 + .../Repositories/EventRepository.cs | 29 +- .../Repositories/EventRepository.cs | 52 + .../Queries/EventReadPageByProjectIdQuery.cs | 49 + .../Queries/EventReadPageBySecretIdQuery.cs | 49 + .../Event/Event_ReadPageByProjectId.sql | 44 + .../Event/Event_ReadPageBySecretId.sql | 44 + .../dbo/Stored Procedures/Event_Create.sql | 9 +- src/Sql/dbo/Tables/Event.sql | 1 + .../Controllers/ProjectsControllerTests.cs | 7 +- ...00_AddProjectEventLogsToEventNewColumn.sql | 16 + ...17_01_AddProjectEventLogsToEventSprocs.sql | 174 + ...0250717_AddingProjectIdToEvent.Designer.cs | 3266 ++++++++++++++++ ...7164642_20250717_AddingProjectIdToEvent.cs | 28 + .../DatabaseContextModelSnapshot.cs | 3 + ...0250717_AddingProjectIdToEvent.Designer.cs | 3272 +++++++++++++++++ ...7164620_20250717_AddingProjectIdToEvent.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...0250717_AddingProjectIdToEvent.Designer.cs | 3255 ++++++++++++++++ ...7164556_20250717_AddingProjectIdToEvent.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + 39 files changed, 10698 insertions(+), 15 deletions(-) create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs create mode 100644 src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByProjectId.sql create mode 100644 src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageBySecretId.sql create mode 100644 util/Migrator/DbScripts/2025-07-17_00_AddProjectEventLogsToEventNewColumn.sql create mode 100644 util/Migrator/DbScripts/2025-07-17_01_AddProjectEventLogsToEventSprocs.sql create mode 100644 util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.cs create mode 100644 util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.cs create mode 100644 util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.cs diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs index 40ae58aa6f..78d90f9525 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs @@ -28,7 +28,10 @@ public class ProjectRepository : Repository> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType) + public async Task> GetManyByOrganizationIdAsync( + Guid organizationId, + Guid userId, + AccessClientType accessType) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs index 14087ddffa..e783e45118 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs @@ -45,6 +45,19 @@ public class SecretRepository : Repository> GetManyTrashedSecretsByIds(IEnumerable ids) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var secrets = await dbContext.Secret + .Where(c => ids.Contains(c.Id) && c.DeletedDate != null) + .Include(c => c.Projects) + .ToListAsync(); + return Mapper.Map>(secrets); + } + } + public async Task> GetManyByOrganizationIdAsync( Guid organizationId, Guid userId, AccessClientType accessType) { @@ -66,10 +79,14 @@ public class SecretRepository : Repository>(secrets); } - public async Task> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType) + public async Task> GetManyDetailsByOrganizationIdAsync( + Guid organizationId, + Guid userId, + AccessClientType accessType) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); + var query = dbContext.Secret .Include(c => c.Projects) .Where(c => c.OrganizationId == organizationId && c.DeletedDate == null) diff --git a/src/Api/AdminConsole/Controllers/EventsController.cs b/src/Api/AdminConsole/Controllers/EventsController.cs index d555c7321d..18199ad8f2 100644 --- a/src/Api/AdminConsole/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Controllers/EventsController.cs @@ -5,9 +5,12 @@ using Bit.Api.Models.Response; using Bit.Api.Utilities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Authorization; @@ -25,6 +28,8 @@ public class EventsController : Controller private readonly IProviderUserRepository _providerUserRepository; private readonly IEventRepository _eventRepository; private readonly ICurrentContext _currentContext; + private readonly ISecretRepository _secretRepository; + private readonly IProjectRepository _projectRepository; public EventsController( IUserService userService, @@ -32,7 +37,9 @@ public class EventsController : Controller IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, IEventRepository eventRepository, - ICurrentContext currentContext) + ICurrentContext currentContext, + ISecretRepository secretRepository, + IProjectRepository projectRepository) { _userService = userService; _cipherRepository = cipherRepository; @@ -40,6 +47,8 @@ public class EventsController : Controller _providerUserRepository = providerUserRepository; _eventRepository = eventRepository; _currentContext = currentContext; + _secretRepository = secretRepository; + _projectRepository = projectRepository; } [HttpGet("")] @@ -104,6 +113,77 @@ public class EventsController : Controller return new ListResponseModel(responses, result.ContinuationToken); } + [HttpGet("~/organization/{orgId}/secrets/{id}/events")] + public async Task> GetSecrets( + Guid id, Guid orgId, + [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, + [FromQuery] string continuationToken = null) + { + if (id == Guid.Empty || orgId == Guid.Empty) + { + throw new NotFoundException(); + } + + var secret = await _secretRepository.GetByIdAsync(id); + var orgIdForVerification = secret?.OrganizationId ?? orgId; + var secretOrg = _currentContext.GetOrganization(orgIdForVerification); + + if (secretOrg == null || !await _currentContext.AccessEventLogs(secretOrg.Id)) + { + throw new NotFoundException(); + } + + bool canViewLogs = false; + + if (secret == null) + { + secret = new Core.SecretsManager.Entities.Secret { Id = id, OrganizationId = orgId }; + canViewLogs = secretOrg.Type is Core.Enums.OrganizationUserType.Admin or Core.Enums.OrganizationUserType.Owner; + } + else + { + canViewLogs = await CanViewSecretsLogs(secret); + } + + if (!canViewLogs) + { + throw new NotFoundException(); + } + + var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end); + var result = await _eventRepository.GetManyBySecretAsync(secret, fromDate, toDate, new PageOptions { ContinuationToken = continuationToken }); + var responses = result.Data.Select(e => new EventResponseModel(e)); + return new ListResponseModel(responses, result.ContinuationToken); + } + + [HttpGet("~/organization/{orgId}/projects/{id}/events")] + public async Task> GetProjects( + Guid id, + Guid orgId, + [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, + [FromQuery] string continuationToken = null) + { + if (id == Guid.Empty || orgId == Guid.Empty) + { + throw new NotFoundException(); + } + + var project = await GetProject(id, orgId); + await ValidateOrganization(project); + + var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end); + var result = await _eventRepository.GetManyByProjectAsync( + project, + fromDate, + toDate, + new PageOptions { ContinuationToken = continuationToken }); + + var responses = result.Data.Select(e => new EventResponseModel(e)); + return new ListResponseModel(responses, result.ContinuationToken); + } + [HttpGet("~/organizations/{orgId}/users/{id}/events")] public async Task> GetOrganizationUser(string orgId, string id, [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null) @@ -157,4 +237,48 @@ public class EventsController : Controller var responses = result.Data.Select(e => new EventResponseModel(e)); return new ListResponseModel(responses, result.ContinuationToken); } + + [ApiExplorerSettings(IgnoreApi = true)] + private async Task ValidateOrganization(Project project) + { + var org = _currentContext.GetOrganization(project.OrganizationId); + + if (org == null || !await _currentContext.AccessEventLogs(org.Id)) + { + throw new NotFoundException(); + } + } + + [ApiExplorerSettings(IgnoreApi = true)] + private async Task GetProject(Guid projectGuid, Guid orgGuid) + { + var project = await _projectRepository.GetByIdAsync(projectGuid); + if (project != null) + { + return project; + } + + var fallbackProject = new Project + { + Id = projectGuid, + OrganizationId = orgGuid + }; + + return fallbackProject; + } + + [ApiExplorerSettings(IgnoreApi = true)] + private async Task CanViewSecretsLogs(Secret secret) + { + if (!_currentContext.AccessSecretsManager(secret.OrganizationId)) + { + throw new NotFoundException(); + } + + var userId = _userService.GetProperUserId(User)!.Value; + var isAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin); + var access = await _secretRepository.AccessToSecretAsync(secret.Id, userId, accessClient); + return access.Read; + } } diff --git a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs index 68695b3ab8..bf02d8b00f 100644 --- a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs @@ -33,6 +33,7 @@ public class EventResponseModel : ResponseModel SystemUser = ev.SystemUser; DomainName = ev.DomainName; SecretId = ev.SecretId; + ProjectId = ev.ProjectId; ServiceAccountId = ev.ServiceAccountId; } @@ -55,5 +56,6 @@ public class EventResponseModel : ResponseModel public EventSystemUser? SystemUser { get; set; } public string DomainName { get; set; } public Guid? SecretId { get; set; } + public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } } diff --git a/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs b/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs index 0609a4d782..3e1de2747a 100644 --- a/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs +++ b/src/Api/AdminConsole/Public/Models/Response/EventResponseModel.cs @@ -28,6 +28,7 @@ public class EventResponseModel : IResponseModel IpAddress = ev.IpAddress; InstallationId = ev.InstallationId; SecretId = ev.SecretId; + ProjectId = ev.ProjectId; ServiceAccountId = ev.ServiceAccountId; } @@ -97,6 +98,11 @@ public class EventResponseModel : IResponseModel /// e68b8629-85eb-4929-92c0-b84464976ba4 public Guid? SecretId { get; set; } /// + /// The unique identifier of the related project that the event describes. + /// + /// e68b8629-85eb-4929-92c0-b84464976ba4 + public Guid? ProjectId { get; set; } + /// /// The unique identifier of the related service account that the event describes. /// /// e68b8629-85eb-4929-92c0-b84464976ba4 diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index 0af122fa57..11b840accf 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -7,6 +7,7 @@ using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Identity; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -29,6 +30,7 @@ public class ProjectsController : Controller private readonly IUpdateProjectCommand _updateProjectCommand; private readonly IDeleteProjectCommand _deleteProjectCommand; private readonly IAuthorizationService _authorizationService; + private readonly IEventService _eventService; public ProjectsController( ICurrentContext currentContext, @@ -38,7 +40,8 @@ public class ProjectsController : Controller ICreateProjectCommand createProjectCommand, IUpdateProjectCommand updateProjectCommand, IDeleteProjectCommand deleteProjectCommand, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IEventService eventService) { _currentContext = currentContext; _userService = userService; @@ -48,6 +51,7 @@ public class ProjectsController : Controller _updateProjectCommand = updateProjectCommand; _deleteProjectCommand = deleteProjectCommand; _authorizationService = authorizationService; + _eventService = eventService; } [HttpGet("organizations/{organizationId}/projects")] @@ -89,6 +93,11 @@ public class ProjectsController : Controller var userId = _userService.GetProperUserId(User).Value; var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.IdentityClientType); + if (result != null) + { + await LogProjectEventAsync(project, EventType.Project_Created); + } + // Creating a project means you have read & write permission. return new ProjectResponseModel(result, true, true); } @@ -106,6 +115,10 @@ public class ProjectsController : Controller } var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id)); + if (result != null) + { + await LogProjectEventAsync(project, EventType.Project_Edited); + } // Updating a project means you have read & write permission. return new ProjectResponseModel(result, true, true); @@ -136,6 +149,8 @@ public class ProjectsController : Controller throw new NotFoundException(); } + await LogProjectEventAsync(project, EventType.Project_Retrieved); + return new ProjectResponseModel(project, access.Read, access.Write); } @@ -175,9 +190,32 @@ public class ProjectsController : Controller } } - await _deleteProjectCommand.DeleteProjects(projectsToDelete); + if (projectsToDelete.Count > 0) + { + await _deleteProjectCommand.DeleteProjects(projectsToDelete); + await LogProjectsEventAsync(projectsToDelete, EventType.Project_Deleted); + } var responses = results.Select(r => new BulkDeleteResponseModel(r.Project.Id, r.Error)); return new ListResponseModel(responses); } + + + private async Task LogProjectsEventAsync(IEnumerable projects, EventType eventType) + { + var userId = _userService.GetProperUserId(User)!.Value; + + switch (_currentContext.IdentityClientType) + { + case IdentityClientType.ServiceAccount: + await _eventService.LogServiceAccountProjectsEventAsync(userId, projects, eventType); + break; + case IdentityClientType.User: + await _eventService.LogUserProjectsEventAsync(userId, projects, eventType); + break; + } + } + + private Task LogProjectEventAsync(Project project, EventType eventType) => + LogProjectsEventAsync(new[] { project }, eventType); } diff --git a/src/Api/SecretsManager/Controllers/SecretsTrashController.cs b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs index 19a84755d8..275e76cc99 100644 --- a/src/Api/SecretsManager/Controllers/SecretsTrashController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs @@ -1,8 +1,12 @@ using Bit.Api.SecretsManager.Models.Response; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Identity; using Bit.Core.SecretsManager.Commands.Trash.Interfaces; +using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -15,17 +19,23 @@ public class TrashController : Controller private readonly ISecretRepository _secretRepository; private readonly IEmptyTrashCommand _emptyTrashCommand; private readonly IRestoreTrashCommand _restoreTrashCommand; + private readonly IUserService _userService; + private readonly IEventService _eventService; public TrashController( ICurrentContext currentContext, ISecretRepository secretRepository, IEmptyTrashCommand emptyTrashCommand, - IRestoreTrashCommand restoreTrashCommand) + IRestoreTrashCommand restoreTrashCommand, + IUserService userService, + IEventService eventService) { _currentContext = currentContext; _secretRepository = secretRepository; _emptyTrashCommand = emptyTrashCommand; _restoreTrashCommand = restoreTrashCommand; + _userService = userService; + _eventService = eventService; } [HttpGet("secrets/{organizationId}/trash")] @@ -58,7 +68,9 @@ public class TrashController : Controller throw new UnauthorizedAccessException(); } + var deletedSecrets = await _secretRepository.GetManyTrashedSecretsByIds(ids); await _emptyTrashCommand.EmptyTrash(organizationId, ids); + await LogSecretsTrashEventAsync(deletedSecrets, EventType.Secret_Permanently_Deleted); } [HttpPost("secrets/{organizationId}/trash/restore")] @@ -75,5 +87,27 @@ public class TrashController : Controller } await _restoreTrashCommand.RestoreTrash(organizationId, ids); + await LogSecretsTrashEventAsync(ids, EventType.Secret_Restored); + } + + private async Task LogSecretsTrashEventAsync(IEnumerable secretIds, EventType eventType) + { + var secrets = await _secretRepository.GetManyByIds(secretIds); + await LogSecretsTrashEventAsync(secrets, eventType); + } + + private async Task LogSecretsTrashEventAsync(IEnumerable secrets, EventType eventType) + { + var userId = _userService.GetProperUserId(User)!.Value; + + switch (_currentContext.IdentityClientType) + { + case IdentityClientType.ServiceAccount: + await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType); + break; + case IdentityClientType.User: + await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType); + break; + } } } diff --git a/src/Core/AdminConsole/Entities/Event.cs b/src/Core/AdminConsole/Entities/Event.cs index 2a6b6664c2..38d8f07b53 100644 --- a/src/Core/AdminConsole/Entities/Event.cs +++ b/src/Core/AdminConsole/Entities/Event.cs @@ -32,6 +32,7 @@ public class Event : ITableObject, IEvent SystemUser = e.SystemUser; DomainName = e.DomainName; SecretId = e.SecretId; + ProjectId = e.ProjectId; ServiceAccountId = e.ServiceAccountId; } @@ -56,6 +57,7 @@ public class Event : ITableObject, IEvent public EventSystemUser? SystemUser { get; set; } public string? DomainName { get; set; } public Guid? SecretId { get; set; } + public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } public void SetNewId() diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/AdminConsole/Enums/EventType.cs index 2359b922d8..32ea4a64e9 100644 --- a/src/Core/AdminConsole/Enums/EventType.cs +++ b/src/Core/AdminConsole/Enums/EventType.cs @@ -93,4 +93,11 @@ public enum EventType : int Secret_Created = 2101, Secret_Edited = 2102, Secret_Deleted = 2103, + Secret_Permanently_Deleted = 2104, + Secret_Restored = 2105, + + Project_Retrieved = 2200, + Project_Created = 2201, + Project_Edited = 2202, + Project_Deleted = 2203, } diff --git a/src/Core/AdminConsole/Models/Data/EventMessage.cs b/src/Core/AdminConsole/Models/Data/EventMessage.cs index 7c2c29f80f..b708c5bd56 100644 --- a/src/Core/AdminConsole/Models/Data/EventMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventMessage.cs @@ -37,5 +37,6 @@ public class EventMessage : IEvent public EventSystemUser? SystemUser { get; set; } public string DomainName { get; set; } public Guid? SecretId { get; set; } + public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs index 410ad67f0e..4ba50aee0d 100644 --- a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs +++ b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs @@ -35,6 +35,7 @@ public class AzureEvent : ITableEntity public int? SystemUser { get; set; } public string DomainName { get; set; } public Guid? SecretId { get; set; } + public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } public EventTableEntity ToEventTableEntity() @@ -65,7 +66,8 @@ public class AzureEvent : ITableEntity SystemUser = SystemUser.HasValue ? (EventSystemUser)SystemUser.Value : null, DomainName = DomainName, SecretId = SecretId, - ServiceAccountId = ServiceAccountId + ServiceAccountId = ServiceAccountId, + ProjectId = ProjectId, }; } } @@ -95,6 +97,7 @@ public class EventTableEntity : IEvent SystemUser = e.SystemUser; DomainName = e.DomainName; SecretId = e.SecretId; + ProjectId = e.ProjectId; ServiceAccountId = e.ServiceAccountId; } @@ -122,6 +125,7 @@ public class EventTableEntity : IEvent public EventSystemUser? SystemUser { get; set; } public string DomainName { get; set; } public Guid? SecretId { get; set; } + public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } public AzureEvent ToAzureEvent() @@ -152,6 +156,7 @@ public class EventTableEntity : IEvent SystemUser = SystemUser.HasValue ? (int)SystemUser.Value : null, DomainName = DomainName, SecretId = SecretId, + ProjectId = ProjectId, ServiceAccountId = ServiceAccountId }; } @@ -218,6 +223,15 @@ public class EventTableEntity : IEvent }); } + if (e.ProjectId.HasValue) + { + entities.Add(new EventTableEntity(e) + { + PartitionKey = pKey, + RowKey = $"ProjectId={e.ProjectId}__Date={dateKey}__Uniquifier={uniquifier}" + }); + } + return entities; } diff --git a/src/Core/AdminConsole/Models/Data/IEvent.cs b/src/Core/AdminConsole/Models/Data/IEvent.cs index 7cdcf06eaf..750fb2e2eb 100644 --- a/src/Core/AdminConsole/Models/Data/IEvent.cs +++ b/src/Core/AdminConsole/Models/Data/IEvent.cs @@ -26,5 +26,6 @@ public interface IEvent EventSystemUser? SystemUser { get; set; } string DomainName { get; set; } Guid? SecretId { get; set; } + Guid? ProjectId { get; set; } Guid? ServiceAccountId { get; set; } } diff --git a/src/Core/AdminConsole/Repositories/IEventRepository.cs b/src/Core/AdminConsole/Repositories/IEventRepository.cs index e39ad33d18..281d6ec8c7 100644 --- a/src/Core/AdminConsole/Repositories/IEventRepository.cs +++ b/src/Core/AdminConsole/Repositories/IEventRepository.cs @@ -1,4 +1,5 @@ using Bit.Core.Models.Data; +using Bit.Core.SecretsManager.Entities; using Bit.Core.Vault.Entities; #nullable enable @@ -11,6 +12,13 @@ public interface IEventRepository PageOptions pageOptions); Task> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate, PageOptions pageOptions); + + Task> GetManyBySecretAsync(Secret secret, DateTime startDate, DateTime endDate, + PageOptions pageOptions); + + Task> GetManyByProjectAsync(Project project, DateTime startDate, DateTime endDate, + PageOptions pageOptions); + Task> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, DateTime startDate, DateTime endDate, PageOptions pageOptions); Task> GetManyByProviderAsync(Guid providerId, DateTime startDate, DateTime endDate, diff --git a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs index 81879ef931..cf661ae346 100644 --- a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs +++ b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs @@ -1,5 +1,6 @@ using Azure.Data.Tables; using Bit.Core.Models.Data; +using Bit.Core.SecretsManager.Entities; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; @@ -34,6 +35,20 @@ public class EventRepository : IEventRepository return await GetManyAsync($"OrganizationId={organizationId}", "Date={0}", startDate, endDate, pageOptions); } + public async Task> GetManyBySecretAsync(Secret secret, + DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + return await GetManyAsync($"OrganizationId={secret.OrganizationId}", + $"SecretId={secret.Id}__Date={{0}}", startDate, endDate, pageOptions); ; + } + + public async Task> GetManyByProjectAsync(Project project, + DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + return await GetManyAsync($"OrganizationId={project.OrganizationId}", + $"ProjectId={project.Id}__Date={{0}}", startDate, endDate, pageOptions); + } + public async Task> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, DateTime startDate, DateTime endDate, PageOptions pageOptions) { diff --git a/src/Core/AdminConsole/Services/IEventService.cs b/src/Core/AdminConsole/Services/IEventService.cs index ba6d4da8f5..80e8e63d8c 100644 --- a/src/Core/AdminConsole/Services/IEventService.cs +++ b/src/Core/AdminConsole/Services/IEventService.cs @@ -35,4 +35,6 @@ public interface IEventService Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null); Task LogUserSecretsEventAsync(Guid userId, IEnumerable secrets, EventType type, DateTime? date = null); Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable secrets, EventType type, DateTime? date = null); + Task LogUserProjectsEventAsync(Guid userId, IEnumerable projects, EventType type, DateTime? date = null); + Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable projects, EventType type, DateTime? date = null); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index e56b3aced4..e0e0e040f1 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -464,6 +464,58 @@ public class EventService : IEventService await _eventWriteService.CreateManyAsync(eventMessages); } + public async Task LogUserProjectsEventAsync(Guid userId, IEnumerable projects, EventType type, DateTime? date = null) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + + foreach (var project in projects) + { + if (!CanUseEvents(orgAbilities, project.OrganizationId)) + { + continue; + } + + var e = new EventMessage(_currentContext) + { + OrganizationId = project.OrganizationId, + Type = type, + ProjectId = project.Id, + UserId = userId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + } + + await _eventWriteService.CreateManyAsync(eventMessages); + } + + public async Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable projects, EventType type, DateTime? date = null) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + + foreach (var project in projects) + { + if (!CanUseEvents(orgAbilities, project.OrganizationId)) + { + continue; + } + + var e = new EventMessage(_currentContext) + { + OrganizationId = project.OrganizationId, + Type = type, + ProjectId = project.Id, + ServiceAccountId = serviceAccountId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + } + + await _eventWriteService.CreateManyAsync(eventMessages); + } + private async Task GetProviderIdAsync(Guid? orgId) { if (_currentContext == null || !orgId.HasValue) diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs index b1ff5b1c4a..e8dd495205 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs @@ -127,4 +127,16 @@ public class NoopEventService : IEventService { return Task.FromResult(0); } + + public Task LogUserProjectsEventAsync(Guid userId, IEnumerable projects, EventType type, + DateTime? date = null) + { + return Task.FromResult(0); + } + + public Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable projects, EventType type, + DateTime? date = null) + { + return Task.FromResult(0); + } } diff --git a/src/Core/SecretsManager/Repositories/ISecretRepository.cs b/src/Core/SecretsManager/Repositories/ISecretRepository.cs index 0456e41ed5..d491bf79d3 100644 --- a/src/Core/SecretsManager/Repositories/ISecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/ISecretRepository.cs @@ -16,6 +16,7 @@ public interface ISecretRepository Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); Task> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable ids); Task> GetManyByIds(IEnumerable ids); + Task> GetManyTrashedSecretsByIds(IEnumerable ids); Task GetByIdAsync(Guid id); Task CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null); Task UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null); diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs index 39f5e3d19e..b54187f8de 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs @@ -105,4 +105,6 @@ public class NoopSecretRepository : ISecretRepository { return Task.FromResult(0); } + + public Task> GetManyTrashedSecretsByIds(IEnumerable ids) => Task.FromResult>([]); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs index 85e3cc7fc2..b034f31f39 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/EventRepository.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Dapper; @@ -41,8 +42,30 @@ public class EventRepository : Repository, IEventRepository }, startDate, endDate, pageOptions); } - public async Task> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, + public async Task> GetManyBySecretAsync(Secret secret, DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + return await GetManyAsync($"[{Schema}].[Event_ReadPageBySecretId]", + new Dictionary + { + ["@SecretId"] = secret.Id + }, startDate, endDate, pageOptions); + + } + + public async Task> GetManyByProjectAsync(Project project, + DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + return await GetManyAsync($"[{Schema}].[Event_ReadPageByProjectId]", + new Dictionary + { + ["@ProjectId"] = project.Id + }, startDate, endDate, pageOptions); + + } + + public async Task> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId, + DateTime startDate, DateTime endDate, PageOptions pageOptions) { return await GetManyAsync($"[{Schema}].[Event_ReadPageByOrganizationIdActingUserId]", new Dictionary @@ -205,6 +228,8 @@ public class EventRepository : Repository, IEventRepository eventsTable.Columns.Add(secretIdColumn); var serviceAccountIdColumn = new DataColumn(nameof(e.ServiceAccountId), typeof(Guid)); eventsTable.Columns.Add(serviceAccountIdColumn); + var projectIdColumn = new DataColumn(nameof(e.ProjectId), typeof(Guid)); + eventsTable.Columns.Add(projectIdColumn); foreach (DataColumn col in eventsTable.Columns) { @@ -237,7 +262,7 @@ public class EventRepository : Repository, IEventRepository row[dateColumn] = ev.Date; row[secretIdColumn] = ev.SecretId.HasValue ? ev.SecretId.Value : DBNull.Value; row[serviceAccountIdColumn] = ev.ServiceAccountId.HasValue ? ev.ServiceAccountId.Value : DBNull.Value; - + row[projectIdColumn] = ev.ProjectId.HasValue ? ev.ProjectId.Value : DBNull.Value; eventsTable.Rows.Add(row); } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs index 55aad0a3c5..0a79782b91 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/EventRepository.cs @@ -1,6 +1,7 @@ using AutoMapper; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories.Queries; using LinqToDB.EntityFrameworkCore; @@ -77,6 +78,57 @@ public class EventRepository : Repository, IEv return result; } + public async Task> GetManyBySecretAsync(Secret secret, + DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + DateTime? beforeDate = null; + if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) && + long.TryParse(pageOptions.ContinuationToken, out var binaryDate)) + { + beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc); + } + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new EventReadPageBySecretQuery(secret, startDate, endDate, beforeDate, pageOptions); + var events = await query.Run(dbContext).ToListAsync(); + + var result = new PagedResult(); + if (events.Any() && events.Count >= pageOptions.PageSize) + { + result.ContinuationToken = events.Last().Date.ToBinary().ToString(); + } + result.Data.AddRange(events); + return result; + } + } + + public async Task> GetManyByProjectAsync(Project project, + DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + DateTime? beforeDate = null; + if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) && + long.TryParse(pageOptions.ContinuationToken, out var binaryDate)) + { + beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc); + } + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new EventReadPageByProjectQuery(project, startDate, endDate, beforeDate, pageOptions); + var events = await query.Run(dbContext).ToListAsync(); + + var result = new PagedResult(); + if (events.Any() && events.Count >= pageOptions.PageSize) + { + result.ContinuationToken = events.Last().Date.ToBinary().ToString(); + } + result.Data.AddRange(events); + return result; + } + } + + public async Task> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions) { DateTime? beforeDate = null; diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs new file mode 100644 index 0000000000..8c66132600 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByProjectIdQuery.cs @@ -0,0 +1,49 @@ +using Bit.Core.Models.Data; +using Bit.Core.SecretsManager.Entities; +using Event = Bit.Infrastructure.EntityFramework.Models.Event; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class EventReadPageByProjectQuery : IQuery +{ + private readonly Project _project; + private readonly DateTime _startDate; + private readonly DateTime _endDate; + private readonly DateTime? _beforeDate; + private readonly PageOptions _pageOptions; + + public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + _project = project; + _startDate = startDate; + _endDate = endDate; + _beforeDate = null; + _pageOptions = pageOptions; + } + + public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions) + { + _project = project; + _startDate = startDate; + _endDate = endDate; + _beforeDate = beforeDate; + _pageOptions = pageOptions; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var emptyGuid = Guid.Empty; + var q = from e in dbContext.Events + where e.Date >= _startDate && + (_beforeDate == null || e.Date < _beforeDate.Value) && + ( + (_project.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) || + (_project.OrganizationId != emptyGuid && e.OrganizationId == _project.OrganizationId) + ) && + e.ProjectId == _project.Id + orderby e.Date descending + select e; + + return q.Take(_pageOptions.PageSize); + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs new file mode 100644 index 0000000000..7ddf0c4589 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageBySecretIdQuery.cs @@ -0,0 +1,49 @@ +using Bit.Core.Models.Data; +using Bit.Core.SecretsManager.Entities; +using Event = Bit.Infrastructure.EntityFramework.Models.Event; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class EventReadPageBySecretQuery : IQuery +{ + private readonly Secret _secret; + private readonly DateTime _startDate; + private readonly DateTime _endDate; + private readonly DateTime? _beforeDate; + private readonly PageOptions _pageOptions; + + public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + _secret = secret; + _startDate = startDate; + _endDate = endDate; + _beforeDate = null; + _pageOptions = pageOptions; + } + + public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions) + { + _secret = secret; + _startDate = startDate; + _endDate = endDate; + _beforeDate = beforeDate; + _pageOptions = pageOptions; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var emptyGuid = Guid.Empty; + var q = from e in dbContext.Events + where e.Date >= _startDate && + (_beforeDate == null || e.Date < _beforeDate.Value) && + ( + (_secret.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) || + (_secret.OrganizationId != emptyGuid && e.OrganizationId == _secret.OrganizationId) + ) && + e.SecretId == _secret.Id + orderby e.Date descending + select e; + + return q.Take(_pageOptions.PageSize); + } +} diff --git a/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByProjectId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByProjectId.sql new file mode 100644 index 0000000000..61a4e55b69 --- /dev/null +++ b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByProjectId.sql @@ -0,0 +1,44 @@ +CREATE PROCEDURE [dbo].[Event_ReadPageByProjectId] + @ProjectId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [ProjectId] = @ProjectId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY +END diff --git a/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageBySecretId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageBySecretId.sql new file mode 100644 index 0000000000..d72d275e64 --- /dev/null +++ b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageBySecretId.sql @@ -0,0 +1,44 @@ +CREATE PROCEDURE [dbo].[Event_ReadPageBySecretId] + @SecretId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [SecretId] = @SecretId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY +END diff --git a/src/Sql/dbo/Stored Procedures/Event_Create.sql b/src/Sql/dbo/Stored Procedures/Event_Create.sql index cd3dd6b6e9..89971bd56f 100644 --- a/src/Sql/dbo/Stored Procedures/Event_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Event_Create.sql @@ -19,7 +19,8 @@ @SystemUser TINYINT = null, @DomainName VARCHAR(256), @SecretId UNIQUEIDENTIFIER = null, - @ServiceAccountId UNIQUEIDENTIFIER = null + @ServiceAccountId UNIQUEIDENTIFIER = null, + @ProjectId UNIQUEIDENTIFIER = null AS BEGIN SET NOCOUNT ON @@ -46,7 +47,8 @@ BEGIN [SystemUser], [DomainName], [SecretId], - [ServiceAccountId] + [ServiceAccountId], + [ProjectId] ) VALUES ( @@ -70,6 +72,7 @@ BEGIN @SystemUser, @DomainName, @SecretId, - @ServiceAccountId + @ServiceAccountId, + @ProjectId ) END diff --git a/src/Sql/dbo/Tables/Event.sql b/src/Sql/dbo/Tables/Event.sql index 1932f103f5..6dfb4392a0 100644 --- a/src/Sql/dbo/Tables/Event.sql +++ b/src/Sql/dbo/Tables/Event.sql @@ -20,6 +20,7 @@ [DomainName] VARCHAR(256) NULL, [SecretId] UNIQUEIDENTIFIER NULL, [ServiceAccountId] UNIQUEIDENTIFIER NULL, + [ProjectId] UNIQUEIDENTIFIER NULL, CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs index a031318b22..9ff4a5e19b 100644 --- a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -317,7 +317,7 @@ public class ProjectsControllerTests [Theory] [BitAutoData] public async Task BulkDeleteProjects_ReturnsAccessDeniedForProjectsWithoutAccess_Success( - SutProvider sutProvider, List data) + SutProvider sutProvider, Guid userId, List data) { var ids = data.Select(project => project.Id).ToList(); @@ -333,6 +333,7 @@ public class ProjectsControllerTests .AuthorizeAsync(Arg.Any(), data.First(), Arg.Any>()).Returns(AuthorizationResult.Failed()); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); var results = await sutProvider.Sut.BulkDeleteAsync(ids); @@ -346,7 +347,7 @@ public class ProjectsControllerTests [Theory] [BitAutoData] - public async Task BulkDeleteProjects_Success(SutProvider sutProvider, List data) + public async Task BulkDeleteProjects_Success(SutProvider sutProvider, Guid userId, List data) { var ids = data.Select(project => project.Id).ToList(); var organizationId = data.First().OrganizationId; @@ -357,7 +358,7 @@ public class ProjectsControllerTests .AuthorizeAsync(Arg.Any(), project, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); } - + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); diff --git a/util/Migrator/DbScripts/2025-07-17_00_AddProjectEventLogsToEventNewColumn.sql b/util/Migrator/DbScripts/2025-07-17_00_AddProjectEventLogsToEventNewColumn.sql new file mode 100644 index 0000000000..15fa548f1d --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-17_00_AddProjectEventLogsToEventNewColumn.sql @@ -0,0 +1,16 @@ +IF COL_LENGTH('[dbo].[Event]', 'ProjectId') IS NULL +BEGIN + EXEC('ALTER TABLE [dbo].[Event] ADD [ProjectId] UNIQUEIDENTIFIER NULL'); +END +GO + +IF OBJECT_ID('[dbo].[EventView]', 'V') IS NOT NULL +BEGIN + DROP VIEW [dbo].[EventView]; +END +GO + +CREATE VIEW [dbo].[EventView] +AS +SELECT * FROM [dbo].[Event]; +GO diff --git a/util/Migrator/DbScripts/2025-07-17_01_AddProjectEventLogsToEventSprocs.sql b/util/Migrator/DbScripts/2025-07-17_01_AddProjectEventLogsToEventSprocs.sql new file mode 100644 index 0000000000..75b143fd21 --- /dev/null +++ b/util/Migrator/DbScripts/2025-07-17_01_AddProjectEventLogsToEventSprocs.sql @@ -0,0 +1,174 @@ +-- Create or alter Event_Create procedure +CREATE OR ALTER PROCEDURE [dbo].[Event_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Type INT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @PolicyId UNIQUEIDENTIFIER, + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserId UNIQUEIDENTIFIER, + @ProviderUserId UNIQUEIDENTIFIER, + @ProviderOrganizationId UNIQUEIDENTIFIER = NULL, + @ActingUserId UNIQUEIDENTIFIER, + @DeviceType SMALLINT, + @IpAddress VARCHAR(50), + @Date DATETIME2(7), + @SystemUser TINYINT = NULL, + @DomainName VARCHAR(256), + @SecretId UNIQUEIDENTIFIER = NULL, + @ServiceAccountId UNIQUEIDENTIFIER = NULL, + @ProjectId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[Event] + ( + [Id], + [Type], + [UserId], + [OrganizationId], + [InstallationId], + [ProviderId], + [CipherId], + [CollectionId], + [PolicyId], + [GroupId], + [OrganizationUserId], + [ProviderUserId], + [ProviderOrganizationId], + [ActingUserId], + [DeviceType], + [IpAddress], + [Date], + [SystemUser], + [DomainName], + [SecretId], + [ServiceAccountId], + [ProjectId] + ) + VALUES + ( + @Id, + @Type, + @UserId, + @OrganizationId, + @InstallationId, + @ProviderId, + @CipherId, + @CollectionId, + @PolicyId, + @GroupId, + @OrganizationUserId, + @ProviderUserId, + @ProviderOrganizationId, + @ActingUserId, + @DeviceType, + @IpAddress, + @Date, + @SystemUser, + @DomainName, + @SecretId, + @ServiceAccountId, + @ProjectId + ); +END +GO + +-- Create or alter Event_ReadPageByProjectId procedure +CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageByProjectId] + @ProjectId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON; + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [ProjectId] = @ProjectId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY; +END +GO + +-- Create or alter Event_ReadPageBySecretId procedure +CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageBySecretId] + @SecretId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON; + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [SecretId] = @SecretId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY; +END +GO diff --git a/util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs b/util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs new file mode 100644 index 0000000000..cd1ef5bdb8 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs @@ -0,0 +1,3266 @@ +// +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("20250717164642_20250717_AddingProjectIdToEvent")] + partial class _20250717_AddingProjectIdToEvent + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ReportData") + .IsRequired() + .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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250717164642_20250717_AddingProjectIdToEvent.cs b/util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.cs new file mode 100644 index 0000000000..2b4bb35b0c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250717_AddingProjectIdToEvent : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProjectId", + table: "Event", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProjectId", + table: "Event"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1b0bf84bfc..2500cc3623 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1295,6 +1295,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("PolicyId") .HasColumnType("char(36)"); + b.Property("ProjectId") + .HasColumnType("char(36)"); + b.Property("ProviderId") .HasColumnType("char(36)"); diff --git a/util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs b/util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs new file mode 100644 index 0000000000..e2c8e26a9a --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs @@ -0,0 +1,3272 @@ +// +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("20250717164620_20250717_AddingProjectIdToEvent")] + partial class _20250717_AddingProjectIdToEvent + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ReportData") + .IsRequired() + .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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250717164620_20250717_AddingProjectIdToEvent.cs b/util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.cs new file mode 100644 index 0000000000..a20fcacd0c --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250717_AddingProjectIdToEvent : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProjectId", + table: "Event", + type: "uuid", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProjectId", + table: "Event"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 2238770810..41f49e6e63 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1300,6 +1300,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("PolicyId") .HasColumnType("uuid"); + b.Property("ProjectId") + .HasColumnType("uuid"); + b.Property("ProviderId") .HasColumnType("uuid"); diff --git a/util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs b/util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs new file mode 100644 index 0000000000..61eafb335d --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs @@ -0,0 +1,3255 @@ +// +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("20250717164556_20250717_AddingProjectIdToEvent")] + partial class _20250717_AddingProjectIdToEvent + { + /// + 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("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("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ReportData") + .IsRequired() + .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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250717164556_20250717_AddingProjectIdToEvent.cs b/util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.cs new file mode 100644 index 0000000000..3136e8ad77 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250717_AddingProjectIdToEvent : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProjectId", + table: "Event", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProjectId", + table: "Event"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 41a179d1b5..11d1517a05 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1284,6 +1284,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("PolicyId") .HasColumnType("TEXT"); + b.Property("ProjectId") + .HasColumnType("TEXT"); + b.Property("ProviderId") .HasColumnType("TEXT"); From cf94438150d114b06f030f5d215bb8c4343669fe Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 20 Aug 2025 11:10:06 -0400 Subject: [PATCH 154/326] [PM-22586/PM-22587] Remove feature flagged logic (#6194) * remove feature flagged logic * remove feature flag * remove OrganizationService.ImportAsync and tests * remove unused function --- .../Controllers/OrganizationController.cs | 19 +- .../Services/IOrganizationService.cs | 4 - .../Implementations/OrganizationService.cs | 222 ------------------ src/Core/Constants.cs | 1 - ...tOrganizationUsersAndGroupsCommandTests.cs | 2 - .../Services/OrganizationServiceTests.cs | 132 ----------- 6 files changed, 3 insertions(+), 377 deletions(-) diff --git a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs index 18afa10ac0..5531204033 100644 --- a/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs +++ b/src/Api/AdminConsole/Public/Controllers/OrganizationController.cs @@ -4,7 +4,6 @@ using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.Models.Public.Response; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Context; using Bit.Core.Exceptions; @@ -57,25 +56,13 @@ public class OrganizationController : Controller throw new BadRequestException("You cannot import this much data at once."); } - if (_featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor)) - { - await _importOrganizationUsersAndGroupsCommand.ImportAsync( + await _importOrganizationUsersAndGroupsCommand.ImportAsync( _currentContext.OrganizationId.Value, model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), - model.OverwriteExisting.GetValueOrDefault()); - } - else - { - await _organizationService.ImportAsync( - _currentContext.OrganizationId.Value, - model.Groups.Select(g => g.ToImportedGroup(_currentContext.OrganizationId.Value)), - model.Members.Where(u => !u.Deleted).Select(u => u.ToImportedOrganizationUser()), - model.Members.Where(u => u.Deleted).Select(u => u.ExternalId), - model.OverwriteExisting.GetValueOrDefault(), - Core.Enums.EventSystemUser.PublicApi); - } + model.OverwriteExisting.GetValueOrDefault() + ); return new OkResult(); } diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index e54e6fee12..8c47ae049c 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -2,7 +2,6 @@ #nullable disable using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -29,9 +28,6 @@ public interface IOrganizationService IEnumerable<(OrganizationUserInvite invite, string externalId)> invites); Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); - Task ImportAsync(Guid organizationId, IEnumerable groups, - IEnumerable newUsers, IEnumerable removeUserExternalIds, - bool overwriteExisting, EventSystemUser eventSystemUser); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 575cdb0230..f418737508 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -5,7 +5,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -27,7 +26,6 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -923,214 +921,6 @@ public class OrganizationService : IOrganizationService : EventType.OrganizationUser_ResetPassword_Withdraw); } - public async Task ImportAsync(Guid organizationId, - IEnumerable groups, - IEnumerable newUsers, - IEnumerable removeUserExternalIds, - bool overwriteExisting, - EventSystemUser eventSystemUser - ) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - if (!organization.UseDirectory) - { - throw new BadRequestException("Organization cannot use directory syncing."); - } - - var newUsersSet = new HashSet(newUsers?.Select(u => u.ExternalId) ?? new List()); - var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var existingExternalUsers = existingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); - var existingExternalUsersIdDict = existingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id); - - // Users - - var events = new List<(OrganizationUserUserDetails ou, EventType e, DateTime? d)>(); - - // Remove Users - if (removeUserExternalIds?.Any() ?? false) - { - var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId); - var removeUsersSet = new HashSet(removeUserExternalIds) - .Except(newUsersSet) - .Where(u => existingUsersDict.TryGetValue(u, out var existingUser) && - existingUser.Type != OrganizationUserType.Owner) - .Select(u => existingUsersDict[u]); - - await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id)); - events.AddRange(removeUsersSet.Select(u => ( - u, - EventType.OrganizationUser_Removed, - (DateTime?)DateTime.UtcNow - )) - ); - } - - if (overwriteExisting) - { - // Remove existing external users that are not in new user set - var usersToDelete = existingExternalUsers.Where(u => - u.Type != OrganizationUserType.Owner && - !newUsersSet.Contains(u.ExternalId) && - existingExternalUsersIdDict.ContainsKey(u.ExternalId)); - await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id)); - events.AddRange(usersToDelete.Select(u => ( - u, - EventType.OrganizationUser_Removed, - (DateTime?)DateTime.UtcNow - )) - ); - foreach (var deletedUser in usersToDelete) - { - existingExternalUsersIdDict.Remove(deletedUser.ExternalId); - } - } - - if (newUsers?.Any() ?? false) - { - // Marry existing users - var existingUsersEmailsDict = existingUsers - .Where(u => string.IsNullOrWhiteSpace(u.ExternalId)) - .ToDictionary(u => u.Email); - var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email); - var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList(); - var usersToUpsert = new List(); - foreach (var user in usersToAttach) - { - var orgUserDetails = existingUsersEmailsDict[user]; - var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserDetails.Id); - if (orgUser != null) - { - orgUser.ExternalId = newUsersEmailsDict[user].ExternalId; - usersToUpsert.Add(orgUser); - existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id); - } - } - - await _organizationUserRepository.UpsertManyAsync(usersToUpsert); - - // Add new users - var existingUsersSet = new HashSet(existingExternalUsersIdDict.Keys); - var usersToAdd = newUsersSet.Except(existingUsersSet).ToList(); - - var seatsAvailable = int.MaxValue; - var enoughSeatsAvailable = true; - if (organization.Seats.HasValue) - { - var seatCounts = - await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); - seatsAvailable = organization.Seats.Value - seatCounts.Total; - enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count; - } - - var hasStandaloneSecretsManager = await _paymentService.HasSecretsManagerStandalone(organization); - - var userInvites = new List<(OrganizationUserInvite, string)>(); - foreach (var user in newUsers) - { - if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email)) - { - continue; - } - - try - { - var invite = new OrganizationUserInvite - { - Emails = new List { user.Email }, - Type = OrganizationUserType.User, - Collections = new List(), - AccessSecretsManager = hasStandaloneSecretsManager - }; - userInvites.Add((invite, user.ExternalId)); - } - catch (BadRequestException) - { - // Thrown when the user is already invited to the organization - continue; - } - } - - var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser, - userInvites); - foreach (var invitedUser in invitedUsers) - { - existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id); - } - } - - - // Groups - if (groups?.Any() ?? false) - { - if (!organization.UseGroups) - { - throw new BadRequestException("Organization cannot use groups."); - } - - var groupsDict = groups.ToDictionary(g => g.Group.ExternalId); - var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId); - var existingExternalGroups = existingGroups - .Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList(); - var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId); - - var newGroups = groups - .Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId)) - .Select(g => g.Group).ToList(); - - var savedGroups = new List(); - foreach (var group in newGroups) - { - group.CreationDate = group.RevisionDate = DateTime.UtcNow; - - savedGroups.Add(await _groupRepository.CreateAsync(group)); - await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds, - existingExternalUsersIdDict); - } - - await _eventService.LogGroupEventsAsync( - savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, - (DateTime?)DateTime.UtcNow))); - - var updateGroups = existingExternalGroups - .Where(g => groupsDict.ContainsKey(g.ExternalId)) - .ToList(); - - if (updateGroups.Any()) - { - var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId); - var existingGroupUsers = groupUsers - .GroupBy(gu => gu.GroupId) - .ToDictionary(g => g.Key, g => new HashSet(g.Select(gr => gr.OrganizationUserId))); - - foreach (var group in updateGroups) - { - var updatedGroup = groupsDict[group.ExternalId].Group; - if (group.Name != updatedGroup.Name) - { - group.RevisionDate = DateTime.UtcNow; - group.Name = updatedGroup.Name; - - await _groupRepository.ReplaceAsync(group); - } - - await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds, - existingExternalUsersIdDict, - existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null); - } - - await _eventService.LogGroupEventsAsync( - updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, - (DateTime?)DateTime.UtcNow))); - } - } - - await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.ou, e.e, eventSystemUser, e.d))); - } public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId) { @@ -1147,18 +937,6 @@ public class OrganizationService : IOrganizationService } } - private async Task UpdateUsersAsync(Group group, HashSet groupUsers, - Dictionary existingUsersIdDict, HashSet existingUsers = null) - { - var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys); - var users = new HashSet(availableUsers.Select(u => existingUsersIdDict[u])); - if (existingUsers != null && existingUsers.Count == users.Count && users.SetEquals(existingUsers)) - { - return; - } - - await _groupRepository.UpdateUsersAsync(group.Id, users); - } public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null) { diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 81b7c59259..2f66df9d22 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -113,7 +113,6 @@ public static class FeatureFlagKeys public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; - public const string ImportAsyncRefactor = "pm-22583-refactor-import-async"; public const string CreateDefaultLocation = "pm-19467-create-default-location"; public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; diff --git a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs index f04fb62c1a..2aea7ac4cd 100644 --- a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -28,8 +28,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture { - featureService.IsEnabled(FeatureFlagKeys.ImportAsyncRefactor) - .Returns(true); featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) .Returns(true); }); diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index f619fed278..e3f26a898d 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -42,139 +42,7 @@ public class OrganizationServiceTests { private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); - [Theory, PaidOrganizationCustomize, BitAutoData] - public async Task OrgImportCreateNewUsers(SutProvider sutProvider, Organization org, List existingUsers, List newUsers) - { - // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks - sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); - sutProvider.Create(); - org.UseDirectory = true; - org.Seats = 10; - newUsers.Add(new ImportedOrganizationUser - { - Email = existingUsers.First().Email, - ExternalId = existingUsers.First().ExternalId - }); - var expectedNewUsersCount = newUsers.Count - 1; - - existingUsers.First().Type = OrganizationUserType.Owner; - - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts - { - Sponsored = 0, - Users = 1 - }); - var organizationUserRepository = sutProvider.GetDependency(); - SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); - - organizationUserRepository.GetManyDetailsByOrganizationAsync(org.Id) - .Returns(existingUsers); - organizationUserRepository.GetCountByOrganizationIdAsync(org.Id) - .Returns(existingUsers.Count); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) - .Returns(true); - sutProvider.GetDependency().ManageUsers(org.Id).Returns(true); - - - await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency().Received(1) - .UpsertManyAsync(Arg.Is>(users => !users.Any())); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default); - - // Create new users - await sutProvider.GetDependency().Received(1) - .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); - - await sutProvider.GetDependency().Received(1) - .SendInvitesAsync( - Arg.Is( - info => info.Users.Length == expectedNewUsersCount && - info.Organization == org)); - - // Send events - await sutProvider.GetDependency().Received(1) - .LogOrganizationUserEventsAsync(Arg.Is>(events => - events.Count() == expectedNewUsersCount)); - } - - [Theory, PaidOrganizationCustomize, BitAutoData] - public async Task OrgImportCreateNewUsersAndMarryExistingUser(SutProvider sutProvider, Organization org, List existingUsers, - List newUsers) - { - // Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks - sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); - sutProvider.Create(); - - org.UseDirectory = true; - org.Seats = newUsers.Count + existingUsers.Count + 1; - var reInvitedUser = existingUsers.First(); - reInvitedUser.ExternalId = null; - newUsers.Add(new ImportedOrganizationUser - { - Email = reInvitedUser.Email, - ExternalId = reInvitedUser.Email, - }); - var expectedNewUsersCount = newUsers.Count - 1; - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(org.Id).Returns(new OrganizationSeatCounts - { - Sponsored = 0, - Users = 1 - }); - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - sutProvider.GetDependency().GetManyDetailsByOrganizationAsync(org.Id) - .Returns(existingUsers); - sutProvider.GetDependency().GetCountByOrganizationIdAsync(org.Id) - .Returns(existingUsers.Count); - sutProvider.GetDependency().GetByIdAsync(reInvitedUser.Id) - .Returns(new OrganizationUser { Id = reInvitedUser.Id }); - - var organizationUserRepository = sutProvider.GetDependency(); - - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(org.Id, Arg.Any>()) - .Returns(true); - - SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository); - - var currentContext = sutProvider.GetDependency(); - currentContext.ManageUsers(org.Id).Returns(true); - - await sutProvider.Sut.ImportAsync(org.Id, null, newUsers, null, false, EventSystemUser.PublicApi); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default, default); - - // Upserted existing user - await sutProvider.GetDependency().Received(1) - .UpsertManyAsync(Arg.Is>(users => users.Count() == 1)); - - // Created and invited new users - await sutProvider.GetDependency().Received(1) - .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); - - await sutProvider.GetDependency().Received(1) - .SendInvitesAsync(Arg.Is(request => - request.Users.Length == expectedNewUsersCount && - request.Organization == org)); - - // Sent events - await sutProvider.GetDependency().Received(1) - .LogOrganizationUserEventsAsync(Arg.Is>(events => - events.Count(e => e.Item2 == EventType.OrganizationUser_Invited) == expectedNewUsersCount)); - } [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, From 22420f595f2f50dd2fc0061743841285258aed22 Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 20 Aug 2025 10:35:51 -0700 Subject: [PATCH 155/326] [PM-20130] Update SecurityTasksNotification email templates (#6200) --- .../Handlebars/SecurityTasksNotification.html.hbs | 4 ++-- .../Handlebars/SecurityTasksNotification.text.hbs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs index 79c3893785..a27575b959 100644 --- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.html.hbs @@ -3,8 +3,8 @@ - Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a - data breach. + Keep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed + in a data breach. diff --git a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs index f6c0921165..8e10afc897 100644 --- a/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs +++ b/src/Core/MailTemplates/Handlebars/SecurityTasksNotification.text.hbs @@ -1,6 +1,6 @@ {{#>SecurityTasksHtmlLayout}} -Keep you and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a data -breach. +Keep yourself and your organization's data safe by changing passwords that are weak, reused, or have been exposed in a +data breach. Launch the Bitwarden extension to review your at-risk passwords. From 58eae7a22019a203f38126849757733fbe784a18 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:11:15 -0500 Subject: [PATCH 156/326] [PM-24552] - Remove code for pm-19956-require-provider-payment-method-during-setup (#6196) * [PM-24552] - remove code for feature flag * pr gate: removing unused and redundant usings/qualifiers --- .../AdminConsole/Services/ProviderService.cs | 5 +- .../Services/ProviderBillingService.cs | 127 ++++++++---------- .../Services/ProviderServiceTests.cs | 5 +- .../Services/ProviderBillingServiceTests.cs | 79 ++--------- .../Implementations/ProviderMigrator.cs | 7 +- .../Services/IProviderBillingService.cs | 2 +- src/Core/Constants.cs | 1 - 7 files changed, 80 insertions(+), 146 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 3300b05531..aa19ad5382 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -120,10 +120,7 @@ public class ProviderService : IProviderService throw new BadRequestException("Both address and postal code are required to set up your provider."); } - var requireProviderPaymentMethodDuringSetup = - _featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); - - if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not + if (tokenizedPaymentSource is not { Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, Token: not null and not "" 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 e02b52cd46..49bcf193b4 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -483,8 +483,10 @@ public class ProviderBillingService( public async Task SetupCustomer( Provider provider, TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource = null) + TokenizedPaymentSource tokenizedPaymentSource) { + ArgumentNullException.ThrowIfNull(tokenizedPaymentSource); + if (taxInfo is not { BillingAddressCountry: not null and not "", @@ -569,56 +571,50 @@ public class ProviderBillingService( options.Coupon = provider.DiscountId; } - var requireProviderPaymentMethodDuringSetup = - featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); - var braintreeCustomerId = ""; - if (requireProviderPaymentMethodDuringSetup) + if (tokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) { - if (tokenizedPaymentSource is not + logger.LogError("Cannot create customer for provider ({ProviderID}) with invalid payment method", provider.Id); + throw new BillingException(); + } + + var (type, token) = tokenizedPaymentSource; + + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (type) + { + case PaymentMethodType.BankAccount: { - Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, - Token: not null and not "" - }) - { - logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id); - throw new BillingException(); - } + var setupIntent = + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token })) + .FirstOrDefault(); - var (type, token) = tokenizedPaymentSource; - - // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (type) - { - case PaymentMethodType.BankAccount: + if (setupIntent == null) { - var setupIntent = - (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token })) - .FirstOrDefault(); + logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id); + throw new BillingException(); + } - if (setupIntent == null) - { - logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id); - throw new BillingException(); - } - - await setupIntentCache.Set(provider.Id, setupIntent.Id); - break; - } - case PaymentMethodType.Card: - { - options.PaymentMethod = token; - options.InvoiceSettings.DefaultPaymentMethod = token; - break; - } - case PaymentMethodType.PayPal: - { - braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token); - options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; - break; - } - } + await setupIntentCache.Set(provider.Id, setupIntent.Id); + break; + } + case PaymentMethodType.Card: + { + options.PaymentMethod = token; + options.InvoiceSettings.DefaultPaymentMethod = token; + break; + } + case PaymentMethodType.PayPal: + { + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token); + options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; + break; + } } try @@ -640,25 +636,22 @@ public class ProviderBillingService( async Task Revert() { - if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null) + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (tokenizedPaymentSource.Type) { - // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (tokenizedPaymentSource.Type) - { - case PaymentMethodType.BankAccount: - { - var setupIntentId = await setupIntentCache.Get(provider.Id); - await stripeAdapter.SetupIntentCancel(setupIntentId, - new SetupIntentCancelOptions { CancellationReason = "abandoned" }); - await setupIntentCache.Remove(provider.Id); - break; - } - case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): - { - await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); - break; - } - } + case PaymentMethodType.BankAccount: + { + var setupIntentId = await setupIntentCache.Get(provider.Id); + await stripeAdapter.SetupIntentCancel(setupIntentId, + new SetupIntentCancelOptions { CancellationReason = "abandoned" }); + await setupIntentCache.Remove(provider.Id); + break; + } + case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } } } } @@ -701,9 +694,6 @@ public class ProviderBillingService( }); } - var requireProviderPaymentMethodDuringSetup = - featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); - var setupIntentId = await setupIntentCache.Get(provider.Id); var setupIntent = !string.IsNullOrEmpty(setupIntentId) @@ -714,10 +704,9 @@ public class ProviderBillingService( : null; var usePaymentMethod = - requireProviderPaymentMethodDuringSetup && - (!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || - customer.Metadata.ContainsKey(BraintreeCustomerIdKey) || - setupIntent.IsUnverifiedBankAccount()); + !string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) || + (customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true) || + (setupIntent?.IsUnverifiedBankAccount() == true); int? trialPeriodDays = provider.Type switch { diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index 608b4b3034..f2ba2fab8f 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -1,6 +1,5 @@ using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.Test.AdminConsole.AutoFixture; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -120,8 +119,6 @@ public class ProviderServiceTests var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; @@ -1194,7 +1191,7 @@ public class ProviderServiceTests private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) => new() { - Items = new List + Items = new List { new() { Id = subscriptionItem.Id, Price = expectedPlanId }, } diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 9af9a71cce..c5b34e45bb 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -901,11 +901,12 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_MissingCountry_ContactSupport( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource) { taxInfo.BillingAddressCountry = null; - await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo)); + await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -916,60 +917,27 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_MissingPostalCode_ContactSupport( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource) { taxInfo.BillingAddressCountry = null; - await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo)); + await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .CustomerGetAsync(Arg.Any(), Arg.Any()); } + [Theory, BitAutoData] - public async Task SetupCustomer_NoPaymentMethod_Success( + public async Task SetupCustomer_NullPaymentSource_ThrowsArgumentNullException( SutProvider sutProvider, Provider provider, TaxInfo taxInfo) { - provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; - - var stripeAdapter = sutProvider.GetDependency(); - - var expected = new Customer - { - Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } - }; - - stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && - o.Email == provider.BillingEmail && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && - o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) - .Returns(expected); - - var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo); - - Assert.Equivalent(expected, actual); + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, null)); } [Theory, BitAutoData] @@ -989,8 +957,6 @@ public class ProviderBillingServiceTests taxInfo.BillingAddressCountry = "AD"; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; @@ -1018,8 +984,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); stripeAdapter.SetupIntentList(Arg.Is(options => options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ @@ -1075,8 +1039,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) .Returns("braintree_customer_id"); @@ -1130,8 +1092,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); stripeAdapter.SetupIntentList(Arg.Is(options => options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ @@ -1187,8 +1147,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) .Returns("braintree_customer_id"); @@ -1241,8 +1199,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); stripeAdapter.CustomerCreateAsync(Arg.Is(o => o.Address.Country == taxInfo.BillingAddressCountry && @@ -1293,8 +1249,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); @@ -1327,7 +1281,8 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource) { provider.Name = "MSP"; @@ -1340,7 +1295,7 @@ public class ProviderBillingServiceTests .Returns((string)null); var actual = await Assert.ThrowsAsync(async () => - await sutProvider.Sut.SetupCustomer(provider, taxInfo)); + await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); Assert.IsType(actual); Assert.Equal("billingTaxIdTypeInferenceError", actual.Message); @@ -1616,8 +1571,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => @@ -1694,8 +1647,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); const string setupIntentId = "seti_123"; @@ -1797,8 +1748,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => @@ -1877,8 +1826,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs index 3a33f96dab..07a057d40c 100644 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs @@ -7,11 +7,13 @@ 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; @@ -253,7 +255,10 @@ public class ProviderMigrator( var taxInfo = await paymentService.GetTaxInfoAsync(sampleOrganization); - var customer = await providerBillingService.SetupCustomer(provider, taxInfo); + // 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, taxInfo, dummyPaymentSource); await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { diff --git a/src/Core/Billing/Providers/Services/IProviderBillingService.cs b/src/Core/Billing/Providers/Services/IProviderBillingService.cs index 518fa1ba98..173249f79f 100644 --- a/src/Core/Billing/Providers/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Providers/Services/IProviderBillingService.cs @@ -88,7 +88,7 @@ public interface IProviderBillingService Task SetupCustomer( Provider provider, TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource = null); + TokenizedPaymentSource tokenizedPaymentSource); /// /// For use during the provider setup process, this method starts a Stripe for the given . diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2f66df9d22..1636faf86f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -153,7 +153,6 @@ public static class FeatureFlagKeys public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; - public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; From 982aaf6f76f766eaf1904378da756b64b35aa62c Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:36:51 -0500 Subject: [PATCH 157/326] [PM-24554] Remove code for pm-20322-allow-trial-length-0 (#6220) * [PM-24554] remove code for feature flag * remove unused using --- src/Core/Constants.cs | 1 - src/Identity/Billing/Controller/AccountsController.cs | 11 +++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1636faf86f..a0ff6f5128 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -154,7 +154,6 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; - public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index f476e4e094..60daebde93 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,7 +1,5 @@ -using Bit.Core; -using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.TrialInitiation.Registration; -using Bit.Core.Services; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Mvc; @@ -11,16 +9,13 @@ namespace Bit.Identity.Billing.Controller; [Route("accounts")] [ExceptionHandlerFilter] public class AccountsController( - ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, - IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller + ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { - var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0); - - var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7; + var trialLength = model.TrialLength ?? 7; var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( model.Email, From 1c98e59003b814d9c9477fea5e47c4c1e9073c7f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 21 Aug 2025 10:44:08 -0700 Subject: [PATCH 158/326] [PM-25050] limit failed 2fa emails to once per hour (#6227) * limit failed 2fa emails to once per hour * Linting. --------- Co-authored-by: Todd Martin --- .../Implementations/HandlebarsMailService.cs | 24 +++++- .../Services/HandlebarsMailServiceTests.cs | 84 ++++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 9dd2dffedf..f06a37fa3b 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -24,6 +24,7 @@ using Bit.Core.Utilities; using Bit.Core.Vault.Models.Data; using Core.Auth.Enums; using HandlebarsDotNet; +using Microsoft.Extensions.Caching.Distributed; namespace Bit.Core.Services; @@ -31,10 +32,12 @@ public class HandlebarsMailService : IMailService { private const string Namespace = "Bit.Core.MailTemplates.Handlebars"; private const string _utcTimeZoneDisplay = "UTC"; + private const string FailedTwoFactorAttemptCacheKeyFormat = "FailedTwoFactorAttemptEmail_{0}"; private readonly GlobalSettings _globalSettings; private readonly IMailDeliveryService _mailDeliveryService; private readonly IMailEnqueuingService _mailEnqueuingService; + private readonly IDistributedCache _distributedCache; private readonly Dictionary> _templateCache = new(); private bool _registeredHelpersAndPartials = false; @@ -42,11 +45,13 @@ public class HandlebarsMailService : IMailService public HandlebarsMailService( GlobalSettings globalSettings, IMailDeliveryService mailDeliveryService, - IMailEnqueuingService mailEnqueuingService) + IMailEnqueuingService mailEnqueuingService, + IDistributedCache distributedCache) { _globalSettings = globalSettings; _mailDeliveryService = mailDeliveryService; _mailEnqueuingService = mailEnqueuingService; + _distributedCache = distributedCache; } public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token) @@ -196,6 +201,16 @@ public class HandlebarsMailService : IMailService public async Task SendFailedTwoFactorAttemptEmailAsync(string email, TwoFactorProviderType failedType, DateTime utcNow, string ip) { + // Check if we've sent this email within the last hour + var cacheKey = string.Format(FailedTwoFactorAttemptCacheKeyFormat, email); + var cachedValue = await _distributedCache.GetAsync(cacheKey); + + if (cachedValue != null) + { + // Email was already sent within the last hour, skip sending + return; + } + var message = CreateDefaultMessage("Failed two-step login attempt detected", email); var model = new FailedAuthAttemptModel() { @@ -211,6 +226,13 @@ public class HandlebarsMailService : IMailService await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempt", model); message.Category = "FailedTwoFactorAttempt"; await _mailDeliveryService.SendEmailAsync(message); + + // Set cache entry with 1 hour expiration to prevent sending again + var cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) + }; + await _distributedCache.SetAsync(cacheKey, [1], cacheOptions); } public async Task SendMasterPasswordHintEmailAsync(string email, string hint) diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 89d9a211e0..849a5130a3 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -2,10 +2,13 @@ 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.Models.Business; using Bit.Core.Entities; +using Bit.Core.Models.Mail; using Bit.Core.Services; using Bit.Core.Settings; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -19,17 +22,93 @@ public class HandlebarsMailServiceTests private readonly GlobalSettings _globalSettings; private readonly IMailDeliveryService _mailDeliveryService; private readonly IMailEnqueuingService _mailEnqueuingService; + private readonly IDistributedCache _distributedCache; public HandlebarsMailServiceTests() { _globalSettings = new GlobalSettings(); _mailDeliveryService = Substitute.For(); _mailEnqueuingService = Substitute.For(); + _distributedCache = Substitute.For(); _sut = new HandlebarsMailService( _globalSettings, _mailDeliveryService, - _mailEnqueuingService + _mailEnqueuingService, + _distributedCache + ); + } + + [Fact] + public async Task SendFailedTwoFactorAttemptEmailAsync_FirstCall_SendsEmail() + { + // Arrange + var email = "test@example.com"; + var failedType = TwoFactorProviderType.Email; + var utcNow = DateTime.UtcNow; + var ip = "192.168.1.1"; + + _distributedCache.GetAsync(Arg.Any()).Returns((byte[])null); + + // Act + await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip); + + // Assert + await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any()); + await _distributedCache.Received(1).SetAsync( + Arg.Is(key => key == $"FailedTwoFactorAttemptEmail_{email}"), + Arg.Any(), + Arg.Any() + ); + } + + [Fact] + public async Task SendFailedTwoFactorAttemptEmailAsync_SecondCallWithinHour_DoesNotSendEmail() + { + // Arrange + var email = "test@example.com"; + var failedType = TwoFactorProviderType.Email; + var utcNow = DateTime.UtcNow; + var ip = "192.168.1.1"; + + // Simulate cache hit (email was already sent) + _distributedCache.GetAsync(Arg.Any()).Returns([1]); + + // Act + await _sut.SendFailedTwoFactorAttemptEmailAsync(email, failedType, utcNow, ip); + + // Assert + await _mailDeliveryService.DidNotReceive().SendEmailAsync(Arg.Any()); + await _distributedCache.DidNotReceive().SetAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task SendFailedTwoFactorAttemptEmailAsync_DifferentEmails_SendsBothEmails() + { + // Arrange + var email1 = "test1@example.com"; + var email2 = "test2@example.com"; + var failedType = TwoFactorProviderType.Email; + var utcNow = DateTime.UtcNow; + var ip = "192.168.1.1"; + + _distributedCache.GetAsync(Arg.Any()).Returns((byte[])null); + + // Act + await _sut.SendFailedTwoFactorAttemptEmailAsync(email1, failedType, utcNow, ip); + await _sut.SendFailedTwoFactorAttemptEmailAsync(email2, failedType, utcNow, ip); + + // Assert + await _mailDeliveryService.Received(2).SendEmailAsync(Arg.Any()); + await _distributedCache.Received(1).SetAsync( + Arg.Is(key => key == $"FailedTwoFactorAttemptEmail_{email1}"), + Arg.Any(), + Arg.Any() + ); + await _distributedCache.Received(1).SetAsync( + Arg.Is(key => key == $"FailedTwoFactorAttemptEmail_{email2}"), + Arg.Any(), + Arg.Any() ); } @@ -137,8 +216,9 @@ public class HandlebarsMailServiceTests }; var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For>()); + var distributedCache = Substitute.For(); - var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService()); + var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService(), distributedCache); var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync"); From c519fa43c670508056d3cedefc339d8c3141d31e Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:54:20 -0500 Subject: [PATCH 159/326] [PM-21878] update gateway/stripe fields for business units (#6186) * [PM-21878] also update gateway/stripe fields for business units * pr feedback: replacing switch with extension method * [PM-21878] prevent invalid stripe ids from crashing the edit provider page * pr feedback: adding service methods to validate stripe ids and added unit tests for the new methods * pr feedback: move validation to SubscriberService and cleanup * pr feedback: use subscriber service to remove dependency on stripe adapter --- .../Controllers/ProvidersController.cs | 29 +++- .../AdminConsole/Models/ProviderEditModel.cs | 12 +- .../Billing/Extensions/BillingExtensions.cs | 4 + .../Billing/Services/ISubscriberService.cs | 18 +++ .../Implementations/SubscriberService.cs | 38 +++++ .../Services/SubscriberServiceTests.cs | 138 ++++++++++++++++++ 6 files changed, 228 insertions(+), 11 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index df333d5d4e..c0c138d0bc 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -22,6 +22,7 @@ using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; +using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -53,6 +54,7 @@ public class ProvidersController : Controller private readonly IPricingClient _pricingClient; private readonly IStripeAdapter _stripeAdapter; private readonly IAccessControlService _accessControlService; + private readonly ISubscriberService _subscriberService; private readonly string _stripeUrl; private readonly string _braintreeMerchantUrl; private readonly string _braintreeMerchantId; @@ -73,7 +75,8 @@ public class ProvidersController : Controller IWebHostEnvironment webHostEnvironment, IPricingClient pricingClient, IStripeAdapter stripeAdapter, - IAccessControlService accessControlService) + IAccessControlService accessControlService, + ISubscriberService subscriberService) { _organizationRepository = organizationRepository; _resellerClientOrganizationSignUpCommand = resellerClientOrganizationSignUpCommand; @@ -93,6 +96,7 @@ public class ProvidersController : Controller _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); _braintreeMerchantId = globalSettings.Braintree.MerchantId; _accessControlService = accessControlService; + _subscriberService = subscriberService; } [RequirePermission(Permission.Provider_List_View)] @@ -299,6 +303,23 @@ public class ProvidersController : Controller model.ToProvider(provider); + // validate the stripe ids to prevent saving a bad one + if (provider.IsBillable()) + { + if (!await _subscriberService.IsValidGatewayCustomerIdAsync(provider)) + { + var oldModel = await GetEditModel(id); + ModelState.AddModelError(nameof(model.GatewayCustomerId), $"Invalid Gateway Customer Id: {model.GatewayCustomerId}"); + return View(oldModel); + } + if (!await _subscriberService.IsValidGatewaySubscriptionIdAsync(provider)) + { + var oldModel = await GetEditModel(id); + ModelState.AddModelError(nameof(model.GatewaySubscriptionId), $"Invalid Gateway Subscription Id: {model.GatewaySubscriptionId}"); + return View(oldModel); + } + } + provider.Enabled = _accessControlService.UserHasPermission(Permission.Provider_CheckEnabledBox) ? model.Enabled : originalProviderStatus; @@ -382,10 +403,8 @@ public class ProvidersController : Controller } var providerPlans = await _providerPlanRepository.GetByProviderId(id); - - var payByInvoice = - _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && - (await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId)).ApprovedToPayByInvoice(); + var payByInvoice = _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && + ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false); return new ProviderEditModel( provider, users, providerOrganizations, diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 450dfbb2fc..a96c3bd236 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Enums; using Bit.SharedWeb.Utilities; @@ -87,14 +88,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim(); existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim(); existingProvider.Enabled = Enabled; - switch (Type) + if (Type.IsStripeSupported()) { - case ProviderType.Msp: - existingProvider.Gateway = Gateway; - existingProvider.GatewayCustomerId = GatewayCustomerId; - existingProvider.GatewaySubscriptionId = GatewaySubscriptionId; - break; + existingProvider.Gateway = Gateway; + existingProvider.GatewayCustomerId = GatewayCustomerId; + existingProvider.GatewaySubscriptionId = GatewaySubscriptionId; } + return existingProvider; } diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index c8a1496726..55db9dde18 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -36,6 +36,10 @@ public static class BillingExtensions Status: ProviderStatusType.Billable }; + // Reseller types do not have Stripe entities + public static bool IsStripeSupported(this ProviderType providerType) => + providerType is ProviderType.Msp or ProviderType.BusinessUnit; + public static bool SupportsConsolidatedBilling(this ProviderType providerType) => providerType is ProviderType.Msp or ProviderType.BusinessUnit; diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index 5f656b2c22..f88727f37b 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -157,4 +157,22 @@ public interface ISubscriberService Task VerifyBankAccount( ISubscriber subscriber, string descriptorCode); + + /// + /// Validates whether the 's exists in the gateway. + /// If the 's is or empty, returns . + /// + /// The subscriber whose gateway customer ID should be validated. + /// if the gateway customer ID is valid or empty; if the customer doesn't exist in the gateway. + /// Thrown when the is . + Task IsValidGatewayCustomerIdAsync(ISubscriber subscriber); + + /// + /// Validates whether the 's exists in the gateway. + /// If the 's is or empty, returns . + /// + /// The subscriber whose gateway subscription ID should be validated. + /// if the gateway subscription ID is valid or empty; if the subscription doesn't exist in the gateway. + /// Thrown when the is . + Task IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber); } diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 73696846ac..53f033de00 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -909,6 +909,44 @@ public class SubscriberService( } } + public async Task IsValidGatewayCustomerIdAsync(ISubscriber subscriber) + { + ArgumentNullException.ThrowIfNull(subscriber); + if (string.IsNullOrEmpty(subscriber.GatewayCustomerId)) + { + // subscribers are allowed to have no customer id as a business rule + return true; + } + try + { + await stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId); + return true; + } + catch (StripeException e) when (e.StripeError.Code == "resource_missing") + { + return false; + } + } + + public async Task IsValidGatewaySubscriptionIdAsync(ISubscriber subscriber) + { + ArgumentNullException.ThrowIfNull(subscriber); + if (string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) + { + // subscribers are allowed to have no subscription id as a business rule + return true; + } + try + { + await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); + return true; + } + catch (StripeException e) when (e.StripeError.Code == "resource_missing") + { + return false; + } + } + #region Shared Utilities private async Task AddBraintreeCustomerIdAsync( diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 3fb134fda8..c41fa81524 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1765,4 +1765,142 @@ public class SubscriberServiceTests } #endregion + + #region IsValidGatewayCustomerIdAsync + + [Theory, BitAutoData] + public async Task IsValidGatewayCustomerIdAsync_NullSubscriber_ThrowsArgumentNullException( + SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.IsValidGatewayCustomerIdAsync(null)); + } + + [Theory, BitAutoData] + public async Task IsValidGatewayCustomerIdAsync_NullGatewayCustomerId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + organization.GatewayCustomerId = null; + + var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization); + + Assert.True(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CustomerGetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task IsValidGatewayCustomerIdAsync_EmptyGatewayCustomerId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + organization.GatewayCustomerId = ""; + + var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization); + + Assert.True(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CustomerGetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task IsValidGatewayCustomerIdAsync_ValidCustomerId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Returns(new Customer()); + + var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization); + + Assert.True(result); + await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId); + } + + [Theory, BitAutoData] + public async Task IsValidGatewayCustomerIdAsync_InvalidCustomerId_ReturnsFalse( + Organization organization, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } }; + stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId).Throws(stripeException); + + var result = await sutProvider.Sut.IsValidGatewayCustomerIdAsync(organization); + + Assert.False(result); + await stripeAdapter.Received(1).CustomerGetAsync(organization.GatewayCustomerId); + } + + #endregion + + #region IsValidGatewaySubscriptionIdAsync + + [Theory, BitAutoData] + public async Task IsValidGatewaySubscriptionIdAsync_NullSubscriber_ThrowsArgumentNullException( + SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(null)); + } + + [Theory, BitAutoData] + public async Task IsValidGatewaySubscriptionIdAsync_NullGatewaySubscriptionId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = null; + + var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization); + + Assert.True(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SubscriptionGetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task IsValidGatewaySubscriptionIdAsync_EmptyGatewaySubscriptionId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + organization.GatewaySubscriptionId = ""; + + var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization); + + Assert.True(result); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SubscriptionGetAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task IsValidGatewaySubscriptionIdAsync_ValidSubscriptionId_ReturnsTrue( + Organization organization, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Returns(new Subscription()); + + var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization); + + Assert.True(result); + await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId); + } + + [Theory, BitAutoData] + public async Task IsValidGatewaySubscriptionIdAsync_InvalidSubscriptionId_ReturnsFalse( + Organization organization, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + var stripeException = new StripeException { StripeError = new StripeError { Code = "resource_missing" } }; + stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId).Throws(stripeException); + + var result = await sutProvider.Sut.IsValidGatewaySubscriptionIdAsync(organization); + + Assert.False(result); + await stripeAdapter.Received(1).SubscriptionGetAsync(organization.GatewaySubscriptionId); + } + + #endregion } From 91bb3c1e6884ec2022e9dee450850c19b213dc46 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Thu, 21 Aug 2025 16:24:16 -0400 Subject: [PATCH 160/326] [PM-24555] Remove Code for PM-21092 (#6198) --- .../RemoveOrganizationFromProviderCommand.cs | 16 +--- .../Services/ProviderBillingService.cs | 25 +---- ...oveOrganizationFromProviderCommandTests.cs | 4 - .../Services/ProviderBillingServiceTests.cs | 10 -- .../Implementations/UpcomingInvoiceHandler.cs | 96 +++++++------------ .../Services/OrganizationBillingService.cs | 27 +----- .../Implementations/SubscriberService.cs | 14 ++- src/Core/Constants.cs | 1 - .../Implementations/StripePaymentService.cs | 71 ++------------ .../Services/SubscriberServiceTests.cs | 3 - 10 files changed, 55 insertions(+), 212 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index ed71b5f438..9ade2d660a 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -137,20 +136,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] }; - var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (setNonUSBusinessUseToReverseCharge) - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - else if (customer.HasRecognizedTaxLocation()) - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = customer.Address.Country == "US" || - customer.TaxIds.Any() - }; - } + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); 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 49bcf193b4..8c0b2c8275 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -3,7 +3,6 @@ using System.Globalization; using Bit.Commercial.Core.Billing.Providers.Models; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -41,7 +40,6 @@ namespace Bit.Commercial.Core.Billing.Providers.Services; public class ProviderBillingService( IBraintreeGateway braintreeGateway, IEventService eventService, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -284,9 +282,7 @@ public class ProviderBillingService( ] }; - var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" }) + if (providerCustomer.Address is not { Country: "US" }) { customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; } @@ -529,9 +525,7 @@ public class ProviderBillingService( } }; - var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US") + if (taxInfo.BillingAddressCountry is not "US") { options.TaxExempt = StripeConstants.TaxExempt.Reverse; } @@ -731,21 +725,8 @@ public class ProviderBillingService( TrialPeriodDays = trialPeriodDays }; - var setNonUSBusinessUseToReverseCharge = - featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (setNonUSBusinessUseToReverseCharge) - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - else if (customer.HasRecognizedTaxLocation()) - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = customer.Address.Country == "US" || - customer.TaxIds.Any() - }; - } + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; try { 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 c9b5b93d5e..9b9c41048b 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -1,5 +1,4 @@ using Bit.Commercial.Core.AdminConsole.Providers; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -332,9 +331,6 @@ public class RemoveOrganizationFromProviderCommandTests Id = "subscription_id" }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); - await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is(options => diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index c5b34e45bb..2bb4c9dcca 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -2,7 +2,6 @@ using System.Net; using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Commercial.Core.Billing.Providers.Services; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -352,9 +351,6 @@ public class ProviderBillingServiceTests CloudRegion = "US" }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); - sutProvider.GetDependency().CustomerCreateAsync(Arg.Is( options => options.Address.Country == providerCustomer.Address.Country && @@ -1250,9 +1246,6 @@ public class ProviderBillingServiceTests var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); - stripeAdapter.CustomerCreateAsync(Arg.Is(o => o.Address.Country == taxInfo.BillingAddressCountry && o.Address.PostalCode == taxInfo.BillingAddressPostalCode && @@ -1827,9 +1820,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); - sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => sub.AutomaticTax.Enabled == true && diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 323eaf5155..9b1d110b5e 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,7 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below + #nullable disable -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; @@ -18,7 +18,6 @@ using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; public class UpcomingInvoiceHandler( - IFeatureService featureService, ILogger logger, IMailService mailService, IOrganizationRepository organizationRepository, @@ -48,8 +47,6 @@ public class UpcomingInvoiceHandler( var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); - var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (organizationId.HasValue) { var organization = await organizationRepository.GetByIdAsync(organizationId.Value); @@ -59,7 +56,7 @@ public class UpcomingInvoiceHandler( return; } - await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); + await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -138,7 +135,7 @@ public class UpcomingInvoiceHandler( return; } - await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); + await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id); await SendUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice); } @@ -164,45 +161,30 @@ public class UpcomingInvoiceHandler( private async Task AlignOrganizationTaxConcernsAsync( Organization organization, Subscription subscription, - string eventId, - bool setNonUSBusinessUseToReverseCharge) + string eventId) { var nonUSBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families && subscription.Customer.Address.Country != "US"; - bool setAutomaticTaxToEnabled; - - if (setNonUSBusinessUseToReverseCharge) + if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { - if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + try { - try - { - await stripeFacade.UpdateCustomer(subscription.CustomerId, - new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}", - organization.Id, - eventId); - } + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}", + organization.Id, + eventId); } - - setAutomaticTaxToEnabled = true; - } - else - { - setAutomaticTaxToEnabled = - subscription.Customer.HasRecognizedTaxLocation() && - (subscription.Customer.Address.Country == "US" || - (nonUSBusinessUse && subscription.Customer.TaxIds.Any())); } - if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + if (!subscription.AutomaticTax.Enabled) { try { @@ -226,41 +208,27 @@ public class UpcomingInvoiceHandler( private async Task AlignProviderTaxConcernsAsync( Provider provider, Subscription subscription, - string eventId, - bool setNonUSBusinessUseToReverseCharge) + string eventId) { - bool setAutomaticTaxToEnabled; - - if (setNonUSBusinessUseToReverseCharge) + if (subscription.Customer.Address.Country != "US" && + subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { - if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + try { - try - { - await stripeFacade.UpdateCustomer(subscription.CustomerId, - new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}", - provider.Id, - eventId); - } + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}", + provider.Id, + eventId); } - - setAutomaticTaxToEnabled = true; - } - else - { - setAutomaticTaxToEnabled = - subscription.Customer.HasRecognizedTaxLocation() && - (subscription.Customer.Address.Country == "US" || - subscription.Customer.TaxIds.Any()); } - if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + if (!subscription.AutomaticTax.Enabled) { try { diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index f32e835dbf..0e42803aaf 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -26,7 +26,6 @@ namespace Bit.Core.Billing.Organizations.Services; public class OrganizationBillingService( IBraintreeGateway braintreeGateway, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -273,11 +272,9 @@ public class OrganizationBillingService( ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - var setNonUSBusinessUseToReverseCharge = - featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (setNonUSBusinessUseToReverseCharge && - planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && + + if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && customerSetup.TaxInformation.Country != "US") { customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; @@ -491,24 +488,10 @@ public class OrganizationBillingService( }; } - var setNonUSBusinessUseToReverseCharge = - featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (setNonUSBusinessUseToReverseCharge && customer.HasBillingLocation()) + if (customer.HasBillingLocation()) { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else if (customer.HasRecognizedTaxLocation()) - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = - subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families || - customer.Address.Country == "US" || - customer.TaxIds.Any() - }; - } - return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } @@ -519,9 +502,7 @@ public class OrganizationBillingService( var customer = await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); - var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (!setNonUSBusinessUseToReverseCharge || subscriptionSetup.PlanType.GetProductTier() is + if (subscriptionSetup.PlanType.GetProductTier() is not (ProductTierType.Teams or ProductTierType.TeamsStarter or ProductTierType.Enterprise)) diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 53f033de00..63a9352020 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -33,7 +33,6 @@ using static StripeConstants; public class SubscriberService( IBraintreeGateway braintreeGateway, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -802,28 +801,27 @@ public class SubscriberService( _ => false }; - var setNonUSBusinessUseToReverseCharge = - featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (setNonUSBusinessUseToReverseCharge && isBusinessUseSubscriber) + + if (isBusinessUseSubscriber) { switch (customer) { case { Address.Country: not "US", - TaxExempt: not StripeConstants.TaxExempt.Reverse + TaxExempt: not TaxExempt.Reverse }: await stripeAdapter.CustomerUpdateAsync(customer.Id, - new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse }); break; case { Address.Country: "US", - TaxExempt: StripeConstants.TaxExempt.Reverse + TaxExempt: TaxExempt.Reverse }: await stripeAdapter.CustomerUpdateAsync(customer.Id, - new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.None }); + new CustomerUpdateOptions { TaxExempt = TaxExempt.None }); break; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a0ff6f5128..7f55a0710d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -154,7 +154,6 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; - public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 1a16731305..440fb5c546 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,11 +1,10 @@ // 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.Models.Business; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; @@ -136,69 +135,17 @@ public class StripePaymentService : IPaymentService if (subscriptionUpdate is CompleteSubscriptionUpdate) { - var setNonUSBusinessUseToReverseCharge = - _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - - if (setNonUSBusinessUseToReverseCharge) - { - if (sub.Customer is - { - Address.Country: not "US", - TaxExempt: not StripeConstants.TaxExempt.Reverse - }) + if (sub.Customer is { - await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, - new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); - } - - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - else if (sub.Customer.HasRecognizedTaxLocation()) + Address.Country: not "US", + TaxExempt: not StripeConstants.TaxExempt.Reverse + }) { - switch (subscriber) - { - case User: - { - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - break; - } - case Organization: - { - if (sub.Customer.Address.Country == "US") - { - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - } - else - { - var familyPriceIds = (await Task.WhenAll( - _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) - .Select(plan => plan.PasswordManager.StripePlanId); - - var updateIsForPersonalUse = updatedItemOptions - .Select(option => option.Price) - .Intersect(familyPriceIds) - .Any(); - - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = updateIsForPersonalUse || sub.Customer.TaxIds.Any() - }; - } - - break; - } - case Provider: - { - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = sub.Customer.Address.Country == "US" || - sub.Customer.TaxIds.Any() - }; - break; - } - } + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); } + + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } if (!subscriptionUpdate.UpdateNeeded(sub)) diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index c41fa81524..0df8d1bfcc 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1695,9 +1695,6 @@ public class SubscriberServiceTests sutProvider.GetDependency().SubscriptionGetAsync(Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); - await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation); await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is( From 50b36bda2a5f24dabdf1c9b22dec9b153b53b0f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:53:36 -0400 Subject: [PATCH 161/326] [deps] Auth: Update Duende.IdentityServer to 7.2.4 (#5683) * [deps] Auth: Update Duende.IdentityServer to 7.2.4 * fix: update namespaces * chore: dotnet format --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike Kottlowski Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- bitwarden_license/src/Scim/Startup.cs | 2 +- .../src/Scim/Utilities/ApiKeyAuthenticationHandler.cs | 2 +- bitwarden_license/src/Sso/Controllers/AccountController.cs | 2 +- .../src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs | 2 +- src/Api/Startup.cs | 2 +- src/Core/Billing/Services/Implementations/LicensingService.cs | 2 +- src/Core/Core.csproj | 2 +- src/Core/Utilities/CoreHelpers.cs | 2 +- src/Events/Startup.cs | 2 +- src/Identity/Controllers/SsoController.cs | 2 +- src/Identity/IdentityServer/ApiResources.cs | 2 +- .../ClientProviders/InstallationClientProvider.cs | 2 +- .../IdentityServer/ClientProviders/InternalClientProvider.cs | 2 +- .../ClientProviders/OrganizationClientProvider.cs | 2 +- .../ClientProviders/SecretsManagerApiKeyProvider.cs | 2 +- .../IdentityServer/ClientProviders/UserClientProvider.cs | 2 +- .../RequestValidators/CustomTokenRequestValidator.cs | 2 +- src/Notifications/Startup.cs | 2 +- src/Notifications/SubjectUserIdProvider.cs | 2 +- src/SharedWeb/Utilities/ServiceCollectionExtensions.cs | 2 +- test/Core.Test/Utilities/CoreHelpersTests.cs | 2 +- .../Endpoints/IdentityServerSsoTests.cs | 2 +- .../Endpoints/IdentityServerTwoFactorTests.cs | 2 +- .../ClientProviders/InstallationClientProviderTests.cs | 2 +- .../ClientProviders/InternalClientProviderTests.cs | 2 +- .../IdentityServer/SendAccessGrantValidatorTests.cs | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index 3fac669eda..edbbf34aea 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -8,7 +8,7 @@ using Bit.Core.Utilities; using Bit.Scim.Context; using Bit.Scim.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.Extensions.DependencyInjection.Extensions; using Stripe; diff --git a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs index 4e7e7ceb7a..6ebffb73cd 100644 --- a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs +++ b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs @@ -3,7 +3,7 @@ using System.Text.Encodings.Web; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Scim.Context; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 7fadc8cb27..30b0d168d0 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -23,10 +23,10 @@ using Bit.Core.Tokens; using Bit.Core.Utilities; using Bit.Sso.Models; using Bit.Sso.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Services; using Duende.IdentityServer.Stores; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; diff --git a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs index c65d7435c3..546bbfb7c9 100644 --- a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs +++ b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs @@ -10,9 +10,9 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Sso.Models; using Bit.Sso.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Infrastructure; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.Options; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 450cb64bad..3a08c4fe8a 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -5,7 +5,7 @@ using Bit.Core.Settings; using AspNetCoreRateLimit; using Stripe; using Bit.Core.Utilities; -using IdentityModel; +using Duende.IdentityModel; using System.Globalization; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request; diff --git a/src/Core/Billing/Services/Implementations/LicensingService.cs b/src/Core/Billing/Services/Implementations/LicensingService.cs index 3734f1747a..81a52158ce 100644 --- a/src/Core/Billing/Services/Implementations/LicensingService.cs +++ b/src/Core/Billing/Services/Implementations/LicensingService.cs @@ -18,7 +18,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 79cd8bf9b8..0dbb8e3023 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -50,7 +50,7 @@ - + diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 14a2ec35e5..64a038be07 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -21,7 +21,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Identity; using Bit.Core.Settings; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.DataProtection; using MimeKit; diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 5fc12854b6..b498bce229 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -5,7 +5,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; namespace Bit.Events; diff --git a/src/Identity/Controllers/SsoController.cs b/src/Identity/Controllers/SsoController.cs index edf57a8b5f..6f843d6ee7 100644 --- a/src/Identity/Controllers/SsoController.cs +++ b/src/Identity/Controllers/SsoController.cs @@ -8,9 +8,9 @@ using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Repositories; using Bit.Identity.Models; +using Duende.IdentityModel; using Duende.IdentityServer; using Duende.IdentityServer.Services; -using IdentityModel; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index a195f01bff..eea53734cb 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -1,7 +1,7 @@ using Bit.Core.Identity; using Bit.Core.IdentityServer; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer; diff --git a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs index 38945016f3..cfa0dee0e6 100644 --- a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs @@ -3,8 +3,8 @@ using Bit.Core.IdentityServer; using Bit.Core.Platform.Installations; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer.ClientProviders; diff --git a/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs index 6d7fdc3459..3cab275a8f 100644 --- a/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs @@ -3,8 +3,8 @@ using System.Diagnostics; using Bit.Core.IdentityServer; using Bit.Core.Settings; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer.ClientProviders; diff --git a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs index e56a135077..2bcae37ee2 100644 --- a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs @@ -5,8 +5,8 @@ using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.IdentityServer; using Bit.Core.Repositories; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer.ClientProviders; diff --git a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs index 0bf28a8258..11022a40e5 100644 --- a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs @@ -5,8 +5,8 @@ using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer.ClientProviders; diff --git a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs index 57699ae415..29d036b893 100644 --- a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs @@ -8,8 +8,8 @@ using Bit.Core.Context; using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.Utilities; +using Duende.IdentityModel; using Duende.IdentityServer.Models; -using IdentityModel; namespace Bit.Identity.IdentityServer.ClientProviders; diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 6223d8dc9c..c7bf1a77db 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -12,10 +12,10 @@ using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Duende.IdentityModel; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using HandlebarsDotNet; -using IdentityModel; using Microsoft.AspNetCore.Identity; #nullable enable diff --git a/src/Notifications/Startup.cs b/src/Notifications/Startup.cs index 440808b78b..c939d0d2fd 100644 --- a/src/Notifications/Startup.cs +++ b/src/Notifications/Startup.cs @@ -3,7 +3,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.SignalR; using Microsoft.IdentityModel.Logging; diff --git a/src/Notifications/SubjectUserIdProvider.cs b/src/Notifications/SubjectUserIdProvider.cs index b0873eb2ec..50d3d1966e 100644 --- a/src/Notifications/SubjectUserIdProvider.cs +++ b/src/Notifications/SubjectUserIdProvider.cs @@ -1,7 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.SignalR; namespace Bit.Notifications; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index c4e7009b4f..51383d650e 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -56,7 +56,7 @@ using Bit.Core.Vault.Services; using Bit.Infrastructure.Dapper; using Bit.Infrastructure.EntityFramework; using DnsClient; -using IdentityModel; +using Duende.IdentityModel; using LaunchDarkly.Sdk.Server; using LaunchDarkly.Sdk.Server.Interfaces; using Microsoft.AspNetCore.Authentication.Cookies; diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index 264a55b6ee..d006df536b 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -9,7 +9,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.DataProtection; using Xunit; diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index b9ab1b0d02..920d3b0ad3 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -17,9 +17,9 @@ using Bit.Core.Repositories; using Bit.Core.Utilities; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.Helpers; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; -using IdentityModel; using Microsoft.EntityFrameworkCore; using NSubstitute; using Xunit; diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 553decd542..a04b8acf19 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -17,9 +17,9 @@ using Bit.Core.Utilities; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; -using IdentityModel; using LinqToDB; using Microsoft.Extensions.Caching.Distributed; using NSubstitute; diff --git a/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs b/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs index 136ff507d2..b53e6ea15f 100644 --- a/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs +++ b/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs @@ -1,7 +1,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Platform.Installations; using Bit.Identity.IdentityServer.ClientProviders; -using IdentityModel; +using Duende.IdentityModel; using NSubstitute; using Xunit; diff --git a/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs b/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs index 23da4b570a..4e5e659218 100644 --- a/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs +++ b/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs @@ -1,7 +1,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Identity.IdentityServer.ClientProviders; -using IdentityModel; +using Duende.IdentityModel; using Xunit; namespace Bit.Identity.Test.IdentityServer.ClientProviders; diff --git a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs index 94f4c1d224..ca45558f19 100644 --- a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs @@ -11,9 +11,9 @@ using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; -using IdentityModel; using NSubstitute; using Xunit; From 3097e7f223ae1481897482c8293d9afb4877ca2e Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 22 Aug 2025 18:02:37 -0400 Subject: [PATCH 162/326] [PM- 22675] Send password auth method (#6228) * feat: add Passwordvalidation * fix: update strings to constants * fix: add customResponse for rust consumption * test: add tests for SendPasswordValidator. fix: update tests for SendAccessGrantValidator * feat: update send access constants. --- src/Core/AssemblyInfo.cs | 1 + .../Enums/SendGrantValidatorResultTypes.cs | 11 - .../Enums/SendPasswordValidatorResultTypes.cs | 9 - .../SendAccess/SendAccessConstants.cs | 73 +++++ .../SendAccess/SendAccessGrantValidator.cs | 45 +-- .../SendPasswordRequestValidator.cs | 26 +- ...endAccessGrantValidatorIntegrationTests.cs | 19 +- ...asswordRequestValidatorIntegrationTests.cs | 209 ++++++++++++ .../SendAccessGrantValidatorTests.cs | 33 +- .../SendPasswordRequestValidatorTests.cs | 297 ++++++++++++++++++ 10 files changed, 647 insertions(+), 76 deletions(-) delete mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs delete mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs create mode 100644 test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs create mode 100644 test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs diff --git a/src/Core/AssemblyInfo.cs b/src/Core/AssemblyInfo.cs index a5edd1a27b..66f5b58ef8 100644 --- a/src/Core/AssemblyInfo.cs +++ b/src/Core/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Core.Test")] +[assembly: InternalsVisibleTo("Identity.IntegrationTest")] diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs deleted file mode 100644 index 343c15bd30..0000000000 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendGrantValidatorResultTypes.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; - -/// -/// These control the results of the SendGrantValidator. -/// -internal enum SendGrantValidatorResultTypes -{ - ValidSendGuid, - MissingSendId, - InvalidSendId -} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs deleted file mode 100644 index 1950ca2978..0000000000 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/Enums/SendPasswordValidatorResultTypes.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; - -/// -/// These control the results of the SendPasswordValidator. -/// -internal enum SendPasswordValidatorResultTypes -{ - RequestPasswordDoesNotMatch -} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs new file mode 100644 index 0000000000..952f4146ed --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs @@ -0,0 +1,73 @@ +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +/// +/// String constants for the Send Access user feature +/// +public static class SendAccessConstants +{ + /// + /// A catch all error type for send access related errors. Used mainly in the + /// + public const string SendAccessError = "send_access_error_type"; + public static class TokenRequest + { + /// + /// used to fetch Send from database. + /// + public const string SendId = "send_id"; + /// + /// used to validate Send protected passwords + /// + public const string ClientB64HashedPassword = "password_hash_b64"; + /// + /// email used to see if email is associated with the Send + /// + public const string Email = "email"; + /// + /// Otp code sent to email associated with the Send + /// + public const string Otp = "otp"; + } + + public static class GrantValidatorResults + { + /// + /// The sendId is valid and the request is well formed. + /// + public const string ValidSendGuid = "valid_send_guid"; + /// + /// The sendId is missing from the request. + /// + public const string MissingSendId = "send_id_required"; + /// + /// The sendId is invalid, does not match a known send. + /// + public const string InvalidSendId = "send_id_invalid"; + } + + public static class PasswordValidatorResults + { + /// + /// The passwordHashB64 does not match the send's password hash. + /// + public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid"; + /// + /// The passwordHashB64 is missing from the request. + /// + public const string RequestPasswordIsRequired = "password_hash_b64_required"; + } + + public static class EmailOtpValidatorResults + { + /// + /// Represents the error code indicating that an email address is required. + /// + public const string EmailRequired = "email_required"; + /// + /// Represents the status indicating that both email and OTP are required, and the OTP has been sent. + /// + public const string EmailOtpSent = "email_and_otp_required_otp_sent"; + } +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs index 020b3ec5d4..7cfa2acd2a 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -6,7 +6,6 @@ using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Core.Utilities; using Bit.Identity.IdentityServer.Enums; -using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -20,11 +19,11 @@ public class SendAccessGrantValidator( { string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess; - private static readonly Dictionary - _sendGrantValidatorErrors = new() + private static readonly Dictionary + _sendGrantValidatorErrorDescriptions = new() { - { SendGrantValidatorResultTypes.MissingSendId, "send_id is required." }, - { SendGrantValidatorResultTypes.InvalidSendId, "send_id is invalid." } + { SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." }, + { SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." } }; @@ -38,7 +37,7 @@ public class SendAccessGrantValidator( } var (sendIdGuid, result) = GetRequestSendId(context); - if (result != SendGrantValidatorResultTypes.ValidSendGuid) + if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid) { context.Result = BuildErrorResult(result); return; @@ -55,7 +54,7 @@ public class SendAccessGrantValidator( // We should only map to password or email + OTP protected. // If user submits password guess for a falsely protected send, then we will return invalid password. // If user submits email + OTP guess for a falsely protected send, then we will return email sent, do not actually send an email. - context.Result = BuildErrorResult(SendGrantValidatorResultTypes.InvalidSendId); + context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId); return; case NotAuthenticated: @@ -64,7 +63,7 @@ public class SendAccessGrantValidator( return; case ResourcePassword rp: - // TODO PM-22675: Validate if the password is correct. + // Validate if the password is correct, or if we need to respond with a 400 stating a password has is required context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid); return; case EmailOtp eo: @@ -84,15 +83,15 @@ public class SendAccessGrantValidator( /// /// request context /// a parsed sendId Guid and success result or a Guid.Empty and error type otherwise - private static (Guid, SendGrantValidatorResultTypes) GetRequestSendId(ExtensionGrantValidationContext context) + private static (Guid, string) GetRequestSendId(ExtensionGrantValidationContext context) { var request = context.Request.Raw; - var sendId = request.Get("send_id"); + var sendId = request.Get(SendAccessConstants.TokenRequest.SendId); // if the sendId is null then the request is the wrong shape and the request is invalid if (sendId == null) { - return (Guid.Empty, SendGrantValidatorResultTypes.MissingSendId); + return (Guid.Empty, SendAccessConstants.GrantValidatorResults.MissingSendId); } // the send_id is not null so the request is the correct shape, so we will attempt to parse it try @@ -102,13 +101,13 @@ public class SendAccessGrantValidator( // Guid.Empty indicates an invalid send_id return invalid grant if (sendGuid == Guid.Empty) { - return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId); + return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId); } - return (sendGuid, SendGrantValidatorResultTypes.ValidSendGuid); + return (sendGuid, SendAccessConstants.GrantValidatorResults.ValidSendGuid); } catch { - return (Guid.Empty, SendGrantValidatorResultTypes.InvalidSendId); + return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId); } } @@ -117,18 +116,26 @@ public class SendAccessGrantValidator( /// /// The error type. /// The error result. - private static GrantValidationResult BuildErrorResult(SendGrantValidatorResultTypes error) + private static GrantValidationResult BuildErrorResult(string error) { return error switch { // Request is the wrong shape - SendGrantValidatorResultTypes.MissingSendId => new GrantValidationResult( + SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult( TokenRequestErrors.InvalidRequest, - errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.MissingSendId]), + errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.MissingSendId], + new Dictionary + { + { SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.MissingSendId} + }), // Request is correct shape but data is bad - SendGrantValidatorResultTypes.InvalidSendId => new GrantValidationResult( + SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult( TokenRequestErrors.InvalidGrant, - errorDescription: _sendGrantValidatorErrors[SendGrantValidatorResultTypes.InvalidSendId]), + errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.InvalidSendId], + new Dictionary + { + { SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.InvalidSendId } + }), // should never get here _ => new GrantValidationResult(TokenRequestErrors.InvalidRequest) }; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs index 194a0aaa5c..3449b4cb56 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs @@ -3,7 +3,6 @@ using Bit.Core.Identity; using Bit.Core.KeyManagement.Sends; using Bit.Core.Tools.Models.Data; using Bit.Identity.IdentityServer.Enums; -using Bit.Identity.IdentityServer.RequestValidators.SendAccess.Enums; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -16,31 +15,44 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher /// /// static object that contains the error messages for the SendPasswordRequestValidator. /// - private static Dictionary _sendPasswordValidatorErrors = new() + private static readonly Dictionary _sendPasswordValidatorErrorDescriptions = new() { - { SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch, "Request Password hash is invalid." } + { SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid." }, + { SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required." } }; public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId) { var request = context.Request.Raw; - var clientHashedPassword = request.Get("password_hash"); + var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword); - if (string.IsNullOrEmpty(clientHashedPassword)) + // It is an invalid request _only_ if the passwordHashB64 is missing which indicated bad shape. + if (clientHashedPassword == null) { + // Request is the wrong shape and doesn't contain a passwordHashB64 field. return new GrantValidationResult( TokenRequestErrors.InvalidRequest, - errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]); + errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired], + new Dictionary + { + { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired } + }); } + // _sendPasswordHasher.PasswordHashMatches checks for an empty string so no need to do it before we make the call. var hashMatches = _sendPasswordHasher.PasswordHashMatches( resourcePassword.Hash, clientHashedPassword); if (!hashMatches) { + // Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty. return new GrantValidationResult( TokenRequestErrors.InvalidGrant, - errorDescription: _sendPasswordValidatorErrors[SendPasswordValidatorResultTypes.RequestPasswordDoesNotMatch]); + errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch], + new Dictionary + { + { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch } + }); } return BuildSendPasswordSuccessResult(sendId); diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs index f27da6e02e..4b8c267861 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Utilities; using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; +using Duende.IdentityModel; using Duende.IdentityServer.Validation; using NSubstitute; using Xunit; @@ -96,8 +97,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }).CreateClient(); var requestBody = new FormUrlEncodedContent([ - new KeyValuePair("grant_type", CustomGrantTypes.SendAccess), - new KeyValuePair("client_id", BitwardenClient.Send) + new KeyValuePair(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), + new KeyValuePair(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send) ]); // Act @@ -105,8 +106,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory // Assert var content = await response.Content.ReadAsStringAsync(); - Assert.Contains("invalid_request", content); - Assert.Contains("send_id is required", content); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + Assert.Contains($"{SendAccessConstants.TokenRequest.SendId} is required", content); } [Fact] @@ -245,16 +246,16 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); var parameters = new List> { - new("grant_type", CustomGrantTypes.SendAccess), - new("client_id", BitwardenClient.Send ), - new("scope", ApiScopes.ApiSendAccess), + new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), + new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ), + new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()), - new("send_id", sendIdBase64) + new(SendAccessConstants.TokenRequest.SendId, sendIdBase64) }; if (!string.IsNullOrEmpty(password)) { - parameters.Add(new("password_hash", password)); + parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password)); } if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail)) diff --git a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs new file mode 100644 index 0000000000..232adb6884 --- /dev/null +++ b/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs @@ -0,0 +1,209 @@ +using Bit.Core.Enums; +using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement.Sends; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.IntegrationTestCommon.Factories; +using Duende.IdentityModel; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.IntegrationTest.RequestValidation; + +public class SendPasswordRequestValidatorIntegrationTests : IClassFixture +{ + private readonly IdentityApplicationFactory _factory; + + public SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken() + { + // Arrange + var sendId = Guid.NewGuid(); + var passwordHash = "stored-password-hash"; + var clientPasswordHash = "client-password-hash"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Enable feature flag + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + // Mock send authentication query + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new ResourcePassword(passwordHash)); + services.AddSingleton(sendAuthQuery); + + // Mock password hasher to return true for matching passwords + var passwordHasher = Substitute.For(); + passwordHasher.PasswordHashMatches(passwordHash, clientPasswordHash) + .Returns(true); + services.AddSingleton(passwordHasher); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, clientPasswordHash); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenResponse.AccessToken, content); + Assert.Contains("bearer", content.ToLower()); + } + + [Fact] + public async Task SendAccess_PasswordProtectedSend_InvalidPassword_ReturnsInvalidGrant() + { + // Arrange + var sendId = Guid.NewGuid(); + var passwordHash = "stored-password-hash"; + var wrongClientPasswordHash = "wrong-client-password-hash"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new ResourcePassword(passwordHash)); + services.AddSingleton(sendAuthQuery); + + // Mock password hasher to return false for wrong passwords + var passwordHasher = Substitute.For(); + passwordHasher.PasswordHashMatches(passwordHash, wrongClientPasswordHash) + .Returns(false); + services.AddSingleton(passwordHasher); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, wrongClientPasswordHash); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); + Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid", content); + } + + [Fact] + public async Task SendAccess_PasswordProtectedSend_MissingPassword_ReturnsInvalidRequest() + { + // Arrange + var sendId = Guid.NewGuid(); + var passwordHash = "stored-password-hash"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new ResourcePassword(passwordHash)); + services.AddSingleton(sendAuthQuery); + + var passwordHasher = Substitute.For(); + services.AddSingleton(passwordHasher); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); // No password + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content); + } + + /// + /// When the password has is empty or whitespace it doesn't get passed to the server when the request is made. + /// This leads to an invalid request error since the absence of the password hash is considered a malformed request. + /// In the case that the passwordB64Hash _is_ empty or whitespace it would be an invalid grant since the request + /// has the correct shape. + /// + [Fact] + public async Task SendAccess_PasswordProtectedSend_EmptyPassword_ReturnsInvalidRequest() + { + // Arrange + var sendId = Guid.NewGuid(); + var passwordHash = "stored-password-hash"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new ResourcePassword(passwordHash)); + services.AddSingleton(sendAuthQuery); + + // Mock password hasher to return false for empty passwords + var passwordHasher = Substitute.For(); + passwordHasher.PasswordHashMatches(passwordHash, string.Empty) + .Returns(false); + services.AddSingleton(passwordHasher); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, string.Empty); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + Assert.Contains($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required", content); + } + + private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId, string passwordHash = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + var parameters = new List> + { + new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), + new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send), + new(SendAccessConstants.TokenRequest.SendId, sendIdBase64), + new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), + new("deviceType", "10") + }; + + if (passwordHash != null) + { + parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash)); + } + + return new FormUrlEncodedContent(parameters); + } +} diff --git a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs index ca45558f19..c3d422c51a 100644 --- a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs @@ -65,7 +65,7 @@ public class SendAccessGrantValidatorTests // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, context.Result.Error); - Assert.Equal("send_id is required.", context.Result.ErrorDescription); + Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is required.", context.Result.ErrorDescription); } [Theory, BitAutoData] @@ -84,7 +84,7 @@ public class SendAccessGrantValidatorTests tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty); // To preserve the CreateTokenRequestBody method for more general usage we over write the sendId - tokenRequest.Raw.Set("send_id", "invalid-guid-format"); + tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format"); context.Request = tokenRequest; // Act @@ -92,7 +92,7 @@ public class SendAccessGrantValidatorTests // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); - Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); } [Theory, BitAutoData] @@ -111,7 +111,7 @@ public class SendAccessGrantValidatorTests // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); - Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); } [Theory, BitAutoData] @@ -135,7 +135,7 @@ public class SendAccessGrantValidatorTests // Assert Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); - Assert.Equal("send_id is invalid.", context.Result.ErrorDescription); + Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); } [Theory, BitAutoData] @@ -297,37 +297,28 @@ public class SendAccessGrantValidatorTests var rawRequestParameters = new NameValueCollection { - { "grant_type", CustomGrantTypes.SendAccess }, - { "client_id", BitwardenClient.Send }, - { "scope", ApiScopes.ApiSendAccess }, + { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, + { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, + { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, { "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() }, - { "send_id", sendIdBase64 } + { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } }; if (passwordHash != null) { - rawRequestParameters.Add("password_hash", passwordHash); + rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash); } if (sendEmail != null) { - rawRequestParameters.Add("send_email", sendEmail); + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail); } if (otpCode != null && sendEmail != null) { - rawRequestParameters.Add("otp_code", otpCode); + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode); } return rawRequestParameters; } - - // we need a list of sendAuthentication methods to test against since we cannot create new objects in the BitAutoData - public static Dictionary SendAuthenticationMethods => new() - { - { "NeverAuthenticate", new NeverAuthenticate() }, // Send doesn't exist or is deleted - { "NotAuthenticated", new NotAuthenticated() }, // Public send, no auth needed - // TODO: PM-22675 - {"ResourcePassword", new ResourcePassword("clientHashedPassword")}; // Password protected send - // TODO: PM-22678 - {"EmailOtp", new EmailOtp(["emailOtp@test.dev"]}; // Email + OTP protected send - }; } diff --git a/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs new file mode 100644 index 0000000000..a776a70178 --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs @@ -0,0 +1,297 @@ +using System.Collections.Specialized; +using Bit.Core.Auth.UserFeatures.SendAccess; +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement.Sends; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer; + +[SutProviderCustomize] +public class SendPasswordRequestValidatorTests +{ + [Theory, BitAutoData] + public void ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required.", result.ErrorDescription); + + // Verify password hasher was not called + sutProvider.GetDependency() + .DidNotReceive() + .PasswordHashMatches(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash) + .Returns(false); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid.", result.ErrorDescription); + + // Verify password hasher was called with correct parameters + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_PasswordHashMatches_ReturnsSuccess( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash) + .Returns(true); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.False(result.IsError); + + var sub = result.Subject; + Assert.Equal(sendId, sub.GetSendId()); + + // Verify claims + Assert.Contains(sub.Claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString()); + Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); + + // Verify password hasher was called + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, string.Empty) + .Returns(false); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + + // Verify password hasher was called with empty string + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, string.Empty); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + var whitespacePassword = " "; + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, whitespacePassword) + .Returns(false); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + + // Verify password hasher was called with whitespace string + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, whitespacePassword); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + var firstPassword = "first-password"; + var secondPassword = "second-password"; + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, firstPassword) + .Returns(true); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + + // Verify password hasher was called with first value + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, $"{firstPassword},{secondPassword}"); + } + + [Theory, BitAutoData] + public void ValidateSendPassword_SuccessResult_ContainsCorrectClaims( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + + // Assert + Assert.False(result.IsError); + var sub = result.Subject; + + var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendId); + Assert.NotNull(sendIdClaim); + Assert.Equal(sendId.ToString(), sendIdClaim.Value); + + var typeClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.Type); + Assert.NotNull(typeClaim); + Assert.Equal(IdentityClientType.Send.ToString(), typeClaim.Value); + } + + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + var sendPasswordHasher = Substitute.For(); + + // Act + var validator = new SendPasswordRequestValidator(sendPasswordHasher); + + // Assert + Assert.NotNull(validator); + } + + private static NameValueCollection CreateValidatedTokenRequest( + Guid sendId, + params string[] passwordHash) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + + var rawRequestParameters = new NameValueCollection + { + { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, + { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, + { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, + { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, + { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } + }; + + if (passwordHash != null && passwordHash.Length > 0) + { + foreach (var hash in passwordHash) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash); + } + } + + return rawRequestParameters; + } +} From 5a712ebb6b18d097affe6054b60b0c6e9ef815a6 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 25 Aug 2025 02:43:24 -0400 Subject: [PATCH 163/326] Xunit v3 (#6241) * Initial v3 Migration * Migrate tests and debug duplicate ids * Debug duplicate ids * Support seeding * remove seeder * Upgrade to latest XUnit.v3 version * Remove Theory changes for now * Remove Theory change from DeviceRepositoryTests * Remove cancellation token additions --- .../DatabaseDataAttribute.cs | 293 +++++++++++------- .../DatabaseTheoryAttribute.cs | 23 +- .../DistributedCacheTests.cs | 4 +- .../Infrastructure.IntegrationTest.csproj | 4 +- .../XUnitLoggerProvider.cs | 47 +++ 5 files changed, 235 insertions(+), 136 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/XUnitLoggerProvider.cs diff --git a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs index 498cc668c0..c458969748 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseDataAttribute.cs @@ -10,129 +10,29 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; +using Xunit; using Xunit.Sdk; +using Xunit.v3; namespace Bit.Infrastructure.IntegrationTest; public class DatabaseDataAttribute : DataAttribute { + private static IConfiguration? _cachedConfiguration; + private static IConfiguration GetConfiguration() + { + return _cachedConfiguration ??= new ConfigurationBuilder() + .AddUserSecrets(optional: true, reloadOnChange: false) + .AddEnvironmentVariables("BW_TEST_") + .AddCommandLine(Environment.GetCommandLineArgs()) + .Build(); + } + + public bool SelfHosted { get; set; } public bool UseFakeTimeProvider { get; set; } public string? MigrationName { get; set; } - public override IEnumerable GetData(MethodInfo testMethod) - { - var parameters = testMethod.GetParameters(); - - var config = DatabaseTheoryAttribute.GetConfiguration(); - - var serviceProviders = GetDatabaseProviders(config); - - foreach (var provider in serviceProviders) - { - var objects = new object[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - objects[i] = provider.GetRequiredService(parameters[i].ParameterType); - } - yield return objects; - } - } - - protected virtual IEnumerable GetDatabaseProviders(IConfiguration config) - { - // This is for the device repository integration testing. - var userRequestExpiration = 15; - - var configureLogging = (ILoggingBuilder builder) => - { - if (!config.GetValue("Quiet")) - { - builder.AddConfiguration(config); - builder.AddConsole(); - builder.AddDebug(); - } - }; - - var databases = config.GetDatabases(); - - foreach (var database in databases) - { - if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) - { - var dapperSqlServerCollection = new ServiceCollection(); - AddCommonServices(dapperSqlServerCollection, configureLogging); - dapperSqlServerCollection.AddDapperRepositories(SelfHosted); - var globalSettings = new GlobalSettings - { - DatabaseProvider = "sqlServer", - SqlServer = new GlobalSettings.SqlSettings - { - ConnectionString = database.ConnectionString, - }, - PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings - { - UserRequestExpiration = TimeSpan.FromMinutes(userRequestExpiration), - } - }; - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(globalSettings); - dapperSqlServerCollection.AddSingleton(database); - dapperSqlServerCollection.AddDistributedSqlServerCache(o => - { - o.ConnectionString = database.ConnectionString; - o.SchemaName = "dbo"; - o.TableName = "Cache"; - }); - - if (!string.IsNullOrEmpty(MigrationName)) - { - AddSqlMigrationTester(dapperSqlServerCollection, database.ConnectionString, MigrationName); - } - - yield return dapperSqlServerCollection.BuildServiceProvider(); - } - else - { - var efCollection = new ServiceCollection(); - AddCommonServices(efCollection, configureLogging); - efCollection.SetupEntityFramework(database.ConnectionString, database.Type); - efCollection.AddPasswordManagerEFRepositories(SelfHosted); - - var globalSettings = new GlobalSettings - { - PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings - { - UserRequestExpiration = TimeSpan.FromMinutes(userRequestExpiration), - } - }; - efCollection.AddSingleton(globalSettings); - efCollection.AddSingleton(globalSettings); - - efCollection.AddSingleton(database); - efCollection.AddSingleton(); - - if (!string.IsNullOrEmpty(MigrationName)) - { - AddEfMigrationTester(efCollection, database.Type, MigrationName); - } - - yield return efCollection.BuildServiceProvider(); - } - } - } - - private void AddCommonServices(IServiceCollection services, Action configureLogging) - { - services.AddLogging(configureLogging); - services.AddDataProtection(); - - if (UseFakeTimeProvider) - { - services.AddSingleton(); - } - } - private void AddSqlMigrationTester(IServiceCollection services, string connectionString, string migrationName) { services.AddSingleton(_ => new SqlMigrationTesterService(connectionString, migrationName)); @@ -146,4 +46,171 @@ public class DatabaseDataAttribute : DataAttribute return new EfMigrationTesterService(dbContext, databaseType, migrationName); }); } + + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) + { + var config = GetConfiguration(); + + HashSet unconfiguredDatabases = + [ + SupportedDatabaseProviders.MySql, + SupportedDatabaseProviders.Postgres, + SupportedDatabaseProviders.Sqlite, + SupportedDatabaseProviders.SqlServer + ]; + + var theories = new List(); + + foreach (var database in config.GetDatabases()) + { + unconfiguredDatabases.Remove(database.Type); + + if (!database.Enabled) + { + var theory = new TheoryDataRow() + .WithSkip("Not-Enabled") + .WithTrait("Database", database.Type.ToString()); + theory.Label = database.Type.ToString(); + theories.Add(theory); + continue; + } + + var services = new ServiceCollection(); + AddCommonServices(services); + + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + // Dapper services + AddDapperServices(services, database); + } + else + { + // Ef services + AddEfServices(services, database); + } + + var serviceProvider = services.BuildServiceProvider(); + disposalTracker.Add(serviceProvider); + + var serviceTheory = new ServiceBasedTheoryDataRow(serviceProvider, testMethod) + .WithTrait("Database", database.Type.ToString()) + .WithTrait("ConnectionString", database.ConnectionString); + + serviceTheory.Label = database.Type.ToString(); + theories.Add(serviceTheory); + } + + foreach (var unconfiguredDatabase in unconfiguredDatabases) + { + var theory = new TheoryDataRow() + .WithSkip("Unconfigured") + .WithTrait("Database", unconfiguredDatabase.ToString()); + theory.Label = unconfiguredDatabase.ToString(); + theories.Add(theory); + } + + return new(theories); + } + + private void AddCommonServices(IServiceCollection services) + { + // Common services + services.AddDataProtection(); + services.AddLogging(logging => + { + logging.AddProvider(new XUnitLoggerProvider()); + }); + if (UseFakeTimeProvider) + { + services.AddSingleton(); + } + } + + private void AddDapperServices(IServiceCollection services, Database database) + { + services.AddDapperRepositories(SelfHosted); + var globalSettings = new GlobalSettings + { + DatabaseProvider = "sqlServer", + SqlServer = new GlobalSettings.SqlSettings + { + ConnectionString = database.ConnectionString, + }, + PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings + { + UserRequestExpiration = TimeSpan.FromMinutes(15), + } + }; + services.AddSingleton(globalSettings); + services.AddSingleton(globalSettings); + services.AddSingleton(database); + services.AddDistributedSqlServerCache(o => + { + o.ConnectionString = database.ConnectionString; + o.SchemaName = "dbo"; + o.TableName = "Cache"; + }); + + if (!string.IsNullOrEmpty(MigrationName)) + { + AddSqlMigrationTester(services, database.ConnectionString, MigrationName); + } + } + + private void AddEfServices(IServiceCollection services, Database database) + { + services.SetupEntityFramework(database.ConnectionString, database.Type); + services.AddPasswordManagerEFRepositories(SelfHosted); + + var globalSettings = new GlobalSettings + { + PasswordlessAuth = new GlobalSettings.PasswordlessAuthSettings + { + UserRequestExpiration = TimeSpan.FromMinutes(15), + }, + }; + services.AddSingleton(globalSettings); + services.AddSingleton(globalSettings); + + services.AddSingleton(database); + services.AddSingleton(); + + if (!string.IsNullOrEmpty(MigrationName)) + { + AddEfMigrationTester(services, database.Type, MigrationName); + } + } + + public override bool SupportsDiscoveryEnumeration() + { + return true; + } + + private class ServiceBasedTheoryDataRow : TheoryDataRowBase + { + private readonly IServiceProvider _serviceProvider; + private readonly MethodInfo _testMethod; + + public ServiceBasedTheoryDataRow(IServiceProvider serviceProvider, MethodInfo testMethod) + { + _serviceProvider = serviceProvider; + _testMethod = testMethod; + } + + protected override object?[] GetData() + { + var parameters = _testMethod.GetParameters(); + + var services = new object?[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + // TODO: Could support keyed services/optional/nullable + services[i] = _serviceProvider.GetRequiredService(parameter.ParameterType); + } + + return services; + } + } } diff --git a/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs b/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs index 1dc6dc76ed..f897220652 100644 --- a/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs +++ b/test/Infrastructure.IntegrationTest/DatabaseTheoryAttribute.cs @@ -1,32 +1,17 @@ -using Microsoft.Extensions.Configuration; +using System.Runtime.CompilerServices; using Xunit; namespace Bit.Infrastructure.IntegrationTest; +[Obsolete("This attribute is no longer needed and can be replaced with a [Theory]")] public class DatabaseTheoryAttribute : TheoryAttribute { - private static IConfiguration? _cachedConfiguration; - public DatabaseTheoryAttribute() { - if (!HasAnyDatabaseSetup()) - { - Skip = "No databases setup."; - } + } - private static bool HasAnyDatabaseSetup() + public DatabaseTheoryAttribute([CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) : base(sourceFilePath, sourceLineNumber) { - var config = GetConfiguration(); - return config.GetDatabases().Length > 0; - } - - public static IConfiguration GetConfiguration() - { - return _cachedConfiguration ??= new ConfigurationBuilder() - .AddUserSecrets(optional: true, reloadOnChange: false) - .AddEnvironmentVariables("BW_TEST_") - .AddCommandLine(Environment.GetCommandLineArgs()) - .Build(); } } diff --git a/test/Infrastructure.IntegrationTest/DistributedCacheTests.cs b/test/Infrastructure.IntegrationTest/DistributedCacheTests.cs index 875f9d16c6..974b8e0c18 100644 --- a/test/Infrastructure.IntegrationTest/DistributedCacheTests.cs +++ b/test/Infrastructure.IntegrationTest/DistributedCacheTests.cs @@ -65,7 +65,7 @@ public class DistributedCacheTests [DatabaseTheory, DatabaseData] public async Task MultipleWritesOnSameKey_ShouldNotThrow(IDistributedCache cache) { - await cache.SetAsync("test-duplicate", "some-value"u8.ToArray()); - await cache.SetAsync("test-duplicate", "some-value"u8.ToArray()); + await cache.SetAsync("test-duplicate", "some-value"u8.ToArray(), TestContext.Current.CancellationToken); + await cache.SetAsync("test-duplicate", "some-value"u8.ToArray(), TestContext.Current.CancellationToken); } } diff --git a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj index 6d9e0d6667..a2215e3453 100644 --- a/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj +++ b/test/Infrastructure.IntegrationTest/Infrastructure.IntegrationTest.csproj @@ -12,8 +12,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Infrastructure.IntegrationTest/XUnitLoggerProvider.cs b/test/Infrastructure.IntegrationTest/XUnitLoggerProvider.cs new file mode 100644 index 0000000000..43310496f5 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/XUnitLoggerProvider.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest; + +public sealed class XUnitLoggerProvider : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(categoryName); + } + + public void Dispose() + { + + } + + private class XUnitLogger : ILogger + { + private readonly string _categoryName; + + public XUnitLogger(string categoryName) + { + _categoryName = categoryName; + } + + public IDisposable? BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (TestContext.Current?.TestOutputHelper is not ITestOutputHelper testOutputHelper) + { + return; + } + + testOutputHelper.WriteLine($"[{_categoryName}] {formatter(state, exception)}"); + } + } +} From 236027fc22cf490ad8317841e44cf3dcf54f0c36 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 25 Aug 2025 11:01:27 +0000 Subject: [PATCH 164/326] Bumped version to 2025.8.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d8af8dc990..3af05be0f1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.8.0 + 2025.8.1 Bit.$(MSBuildProjectName) enable From a7fc89a5bb5a85855251fdd34ca2886621099c4f Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Mon, 25 Aug 2025 14:34:06 -0500 Subject: [PATCH 165/326] Removing extra semi colon (#6246) --- .../AdminConsole/Repositories/TableStorage/EventRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs index cf661ae346..c9c803b5b2 100644 --- a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs +++ b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs @@ -39,7 +39,7 @@ public class EventRepository : IEventRepository DateTime startDate, DateTime endDate, PageOptions pageOptions) { return await GetManyAsync($"OrganizationId={secret.OrganizationId}", - $"SecretId={secret.Id}__Date={{0}}", startDate, endDate, pageOptions); ; + $"SecretId={secret.Id}__Date={{0}}", startDate, endDate, pageOptions); } public async Task> GetManyByProjectAsync(Project project, From a4c4d0157bff55cc665518cda6afec53943e89c5 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:00:41 -0700 Subject: [PATCH 166/326] check for UserId in ReplaceAsync (#6176) --- .../Vault/Repositories/CipherRepository.cs | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 11a74a8097..3fae537a1e 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -553,59 +553,61 @@ public class CipherRepository : Repository(cipher.UserId.Value.ToString(), true), }); - cipher.Favorites = JsonSerializer.Serialize(jsonObject); + cipher.Favorites = JsonSerializer.Serialize(jsonObject); + } + else + { + var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites); + favorites.Add(cipher.UserId.Value, true); + cipher.Favorites = JsonSerializer.Serialize(favorites); + } } else { - var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites); - favorites.Add(cipher.UserId.Value, true); - cipher.Favorites = JsonSerializer.Serialize(favorites); - } - } - else - { - if (cipher.Favorites != null && cipher.Favorites.Contains(cipher.UserId.Value.ToString())) - { - var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites); - favorites.Remove(cipher.UserId.Value); - cipher.Favorites = JsonSerializer.Serialize(favorites); - } - } - if (cipher.FolderId.HasValue) - { - if (cipher.Folders == null) - { - var jsonObject = new JsonObject(new[] + if (cipher.Favorites != null && cipher.Favorites.Contains(cipher.UserId.Value.ToString())) { + var favorites = CoreHelpers.LoadClassFromJsonData>(cipher.Favorites); + favorites.Remove(cipher.UserId.Value); + cipher.Favorites = JsonSerializer.Serialize(favorites); + } + } + if (cipher.FolderId.HasValue) + { + if (cipher.Folders == null) + { + var jsonObject = new JsonObject(new[] + { new KeyValuePair(cipher.UserId.Value.ToString(), cipher.FolderId), }); - cipher.Folders = JsonSerializer.Serialize(jsonObject); + cipher.Folders = JsonSerializer.Serialize(jsonObject); + } + else + { + var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Folders); + folders.Add(cipher.UserId.Value, cipher.FolderId.Value); + cipher.Folders = JsonSerializer.Serialize(folders); + } } else { - var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Folders); - folders.Add(cipher.UserId.Value, cipher.FolderId.Value); - cipher.Folders = JsonSerializer.Serialize(folders); + if (cipher.Folders != null && cipher.Folders.Contains(cipher.UserId.Value.ToString())) + { + var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Folders); + folders.Remove(cipher.UserId.Value); + cipher.Folders = JsonSerializer.Serialize(folders); + } } } - else - { - if (cipher.Folders != null && cipher.Folders.Contains(cipher.UserId.Value.ToString())) - { - var folders = CoreHelpers.LoadClassFromJsonData>(cipher.Folders); - folders.Remove(cipher.UserId.Value); - cipher.Folders = JsonSerializer.Serialize(folders); - } - } - // Check if this cipher is a part of an organization, and if so do // not save the UserId into the database. This must be done after we // set the user specific data like Folders and Favorites because From 004e6285a16b395eb713c6b8b46a7ccfbbf1f78c Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:35:23 -0500 Subject: [PATCH 167/326] PM-21024 ChangePasswordUri controller + service (#5845) * add ChangePasswordUri controller and service to Icons * add individual settings for change password uri * add logging to change password uri controller * use custom http client that follows redirects * add ChangePasswordUriService tests * remove unneeded null check * fix copy pasta - changePasswordUriSettings * add `HelpUsersUpdatePasswords` policy * Remove policy for change password uri - this was removed from scope * fix nullable warnings --- .../ChangePasswordUriController.cs | 89 +++++++++++++++ src/Icons/Models/ChangePasswordUriResponse.cs | 11 ++ src/Icons/Models/ChangePasswordUriSettings.cs | 8 ++ .../Services/ChangePasswordUriService.cs | 89 +++++++++++++++ .../Services/IChangePasswordUriService.cs | 6 + src/Icons/Startup.cs | 8 ++ src/Icons/Util/ServiceCollectionExtension.cs | 19 +++ src/Icons/appsettings.json | 5 + .../Services/ChangePasswordUriServiceTests.cs | 108 ++++++++++++++++++ 9 files changed, 343 insertions(+) create mode 100644 src/Icons/Controllers/ChangePasswordUriController.cs create mode 100644 src/Icons/Models/ChangePasswordUriResponse.cs create mode 100644 src/Icons/Models/ChangePasswordUriSettings.cs create mode 100644 src/Icons/Services/ChangePasswordUriService.cs create mode 100644 src/Icons/Services/IChangePasswordUriService.cs create mode 100644 test/Icons.Test/Services/ChangePasswordUriServiceTests.cs diff --git a/src/Icons/Controllers/ChangePasswordUriController.cs b/src/Icons/Controllers/ChangePasswordUriController.cs new file mode 100644 index 0000000000..3f2bc91cf2 --- /dev/null +++ b/src/Icons/Controllers/ChangePasswordUriController.cs @@ -0,0 +1,89 @@ +using Bit.Icons.Models; +using Bit.Icons.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; + +namespace Bit.Icons.Controllers; + +[Route("change-password-uri")] +public class ChangePasswordUriController : Controller +{ + private readonly IMemoryCache _memoryCache; + private readonly IDomainMappingService _domainMappingService; + private readonly IChangePasswordUriService _changePasswordService; + private readonly ChangePasswordUriSettings _changePasswordSettings; + private readonly ILogger _logger; + + public ChangePasswordUriController( + IMemoryCache memoryCache, + IDomainMappingService domainMappingService, + IChangePasswordUriService changePasswordService, + ChangePasswordUriSettings changePasswordUriSettings, + ILogger logger) + { + _memoryCache = memoryCache; + _domainMappingService = domainMappingService; + _changePasswordService = changePasswordService; + _changePasswordSettings = changePasswordUriSettings; + _logger = logger; + } + + [HttpGet("config")] + public IActionResult GetConfig() + { + return new JsonResult(new + { + _changePasswordSettings.CacheEnabled, + _changePasswordSettings.CacheHours, + _changePasswordSettings.CacheSizeLimit + }); + } + + [HttpGet] + public async Task Get([FromQuery] string uri) + { + if (string.IsNullOrWhiteSpace(uri)) + { + return new BadRequestResult(); + } + + var uriHasProtocol = uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + + var url = uriHasProtocol ? uri : $"https://{uri}"; + if (!Uri.TryCreate(url, UriKind.Absolute, out var validUri)) + { + return new BadRequestResult(); + } + + var domain = validUri.Host; + + var mappedDomain = _domainMappingService.MapDomain(domain); + if (!_changePasswordSettings.CacheEnabled || !_memoryCache.TryGetValue(mappedDomain, out string? changePasswordUri)) + { + var result = await _changePasswordService.GetChangePasswordUri(domain); + if (result == null) + { + _logger.LogWarning("Null result returned for {0}.", domain); + changePasswordUri = null; + } + else + { + changePasswordUri = result; + } + + if (_changePasswordSettings.CacheEnabled) + { + _logger.LogInformation("Cache uri for {0}.", domain); + _memoryCache.Set(mappedDomain, changePasswordUri, new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = new TimeSpan(_changePasswordSettings.CacheHours, 0, 0), + Size = changePasswordUri?.Length ?? 0, + Priority = changePasswordUri == null ? CacheItemPriority.High : CacheItemPriority.Normal + }); + } + } + + return Ok(new ChangePasswordUriResponse(changePasswordUri)); + } +} diff --git a/src/Icons/Models/ChangePasswordUriResponse.cs b/src/Icons/Models/ChangePasswordUriResponse.cs new file mode 100644 index 0000000000..def6806bd3 --- /dev/null +++ b/src/Icons/Models/ChangePasswordUriResponse.cs @@ -0,0 +1,11 @@ +namespace Bit.Icons.Models; + +public class ChangePasswordUriResponse +{ + public string? uri { get; set; } + + public ChangePasswordUriResponse(string? uri) + { + this.uri = uri; + } +} diff --git a/src/Icons/Models/ChangePasswordUriSettings.cs b/src/Icons/Models/ChangePasswordUriSettings.cs new file mode 100644 index 0000000000..bcb804f4e0 --- /dev/null +++ b/src/Icons/Models/ChangePasswordUriSettings.cs @@ -0,0 +1,8 @@ +namespace Bit.Icons.Models; + +public class ChangePasswordUriSettings +{ + public virtual bool CacheEnabled { get; set; } + public virtual int CacheHours { get; set; } + public virtual long? CacheSizeLimit { get; set; } +} diff --git a/src/Icons/Services/ChangePasswordUriService.cs b/src/Icons/Services/ChangePasswordUriService.cs new file mode 100644 index 0000000000..6f2b73efff --- /dev/null +++ b/src/Icons/Services/ChangePasswordUriService.cs @@ -0,0 +1,89 @@ +namespace Bit.Icons.Services; + +public class ChangePasswordUriService : IChangePasswordUriService +{ + private readonly HttpClient _httpClient; + + public ChangePasswordUriService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient("ChangePasswordUri"); + } + + /// + /// Fetches the well-known change password URL for the given domain. + /// + /// + /// + public async Task GetChangePasswordUri(string domain) + { + if (string.IsNullOrWhiteSpace(domain)) + { + return null; + } + + var hasReliableStatusCode = await HasReliableHttpStatusCode(domain); + var wellKnownChangePasswordUrl = await GetWellKnownChangePasswordUrl(domain); + + + if (hasReliableStatusCode && wellKnownChangePasswordUrl != null) + { + return wellKnownChangePasswordUrl; + } + + // Reliable well-known URL criteria not met, return null + return null; + } + + /// + /// Checks if the server returns a non-200 status code for a resource that should not exist. + // See https://w3c.github.io/webappsec-change-password-url/response-code-reliability.html#semantics + /// + /// The domain of the URL to check + /// True when the domain responds with a non-ok response + private async Task HasReliableHttpStatusCode(string urlDomain) + { + try + { + var url = new UriBuilder(urlDomain) + { + Path = "/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200" + }; + + var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()); + + var response = await _httpClient.SendAsync(request); + return !response.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + /// + /// Builds a well-known change password URL for the given origin. Attempts to fetch the URL to ensure a valid response + /// is returned. Returns null if the request throws or the response is not 200 OK. + /// See https://w3c.github.io/webappsec-change-password-url/ + /// + /// The domain of the URL to check + /// The well-known change password URL if valid, otherwise null + private async Task GetWellKnownChangePasswordUrl(string urlDomain) + { + try + { + var url = new UriBuilder(urlDomain) + { + Path = "/.well-known/change-password" + }; + + var request = new HttpRequestMessage(HttpMethod.Get, url.ToString()); + + var response = await _httpClient.SendAsync(request); + return response.IsSuccessStatusCode ? url.ToString() : null; + } + catch + { + return null; + } + } +} diff --git a/src/Icons/Services/IChangePasswordUriService.cs b/src/Icons/Services/IChangePasswordUriService.cs new file mode 100644 index 0000000000..f010255db5 --- /dev/null +++ b/src/Icons/Services/IChangePasswordUriService.cs @@ -0,0 +1,6 @@ +namespace Bit.Icons.Services; + +public interface IChangePasswordUriService +{ + Task GetChangePasswordUri(string domain); +} diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs index 4695c320e9..16bbdef553 100644 --- a/src/Icons/Startup.cs +++ b/src/Icons/Startup.cs @@ -2,6 +2,7 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Icons.Extensions; +using Bit.Icons.Models; using Bit.SharedWeb.Utilities; using Microsoft.Net.Http.Headers; @@ -27,8 +28,11 @@ public class Startup // Settings var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment); var iconsSettings = new IconsSettings(); + var changePasswordUriSettings = new ChangePasswordUriSettings(); ConfigurationBinder.Bind(Configuration.GetSection("IconsSettings"), iconsSettings); + ConfigurationBinder.Bind(Configuration.GetSection("ChangePasswordUriSettings"), changePasswordUriSettings); services.AddSingleton(s => iconsSettings); + services.AddSingleton(s => changePasswordUriSettings); // Http client services.ConfigureHttpClients(); @@ -41,6 +45,10 @@ public class Startup { options.SizeLimit = iconsSettings.CacheSizeLimit; }); + services.AddMemoryCache(options => + { + options.SizeLimit = changePasswordUriSettings.CacheSizeLimit; + }); // Services services.AddServices(); diff --git a/src/Icons/Util/ServiceCollectionExtension.cs b/src/Icons/Util/ServiceCollectionExtension.cs index 5492cda0cf..3bd3537198 100644 --- a/src/Icons/Util/ServiceCollectionExtension.cs +++ b/src/Icons/Util/ServiceCollectionExtension.cs @@ -28,6 +28,24 @@ public static class ServiceCollectionExtension AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, }); + + // The CreatePasswordUri handler wants similar headers as Icons to portray coming from a browser but + // needs to follow redirects to get the final URL. + services.AddHttpClient("ChangePasswordUri", client => + { + client.Timeout = TimeSpan.FromSeconds(20); + client.MaxResponseContentBufferSize = 5000000; // 5 MB + // Let's add some headers to look like we're coming from a web browser request. Some websites + // will block our request without these. + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); + client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.8"); + client.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); + client.DefaultRequestHeaders.Add("Pragma", "no-cache"); + }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }); } public static void AddHtmlParsing(this IServiceCollection services) @@ -40,5 +58,6 @@ public static class ServiceCollectionExtension services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } } diff --git a/src/Icons/appsettings.json b/src/Icons/appsettings.json index 6b4e2992e0..5e1113b150 100644 --- a/src/Icons/appsettings.json +++ b/src/Icons/appsettings.json @@ -6,5 +6,10 @@ "cacheEnabled": true, "cacheHours": 24, "cacheSizeLimit": null + }, + "changePasswordUriSettings": { + "cacheEnabled": true, + "cacheHours": 24, + "cacheSizeLimit": null } } diff --git a/test/Icons.Test/Services/ChangePasswordUriServiceTests.cs b/test/Icons.Test/Services/ChangePasswordUriServiceTests.cs new file mode 100644 index 0000000000..53b883733b --- /dev/null +++ b/test/Icons.Test/Services/ChangePasswordUriServiceTests.cs @@ -0,0 +1,108 @@ +using System.Net; +using Bit.Icons.Services; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; + +namespace Bit.Icons.Test.Services; + +public class ChangePasswordUriServiceTests : ServiceTestBase +{ + [Theory] + [InlineData("https://example.com", "https://example.com:443/.well-known/change-password")] + public async Task GetChangePasswordUri_WhenBothChecksPass_ReturnsWellKnownUrl(string domain, string expectedUrl) + { + // Arrange + var mockedHandler = new MockedHttpMessageHandler(); + + var nonExistentUrl = $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200"; + var changePasswordUrl = $"{domain}/.well-known/change-password"; + + // Mock the response for the resource-that-should-not-exist request (returns 404) + mockedHandler + .When(nonExistentUrl) + .RespondWith(HttpStatusCode.NotFound) + .WithContent(new StringContent("Not found")); + + // Mock the response for the change-password request (returns 200) + mockedHandler + .When(changePasswordUrl) + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent("Ok")); + + var mockHttpFactory = Substitute.For(); + mockHttpFactory.CreateClient("ChangePasswordUri").Returns(mockedHandler.ToHttpClient()); + + var service = new ChangePasswordUriService(mockHttpFactory); + + var result = await service.GetChangePasswordUri(domain); + + Assert.Equal(expectedUrl, result); + } + + [Theory] + [InlineData("https://example.com")] + public async Task GetChangePasswordUri_WhenResourceThatShouldNotExistReturns200_ReturnsNull(string domain) + { + var mockHttpFactory = Substitute.For(); + var mockedHandler = new MockedHttpMessageHandler(); + + mockedHandler + .When(HttpMethod.Get, $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent("Ok")); + + mockedHandler + .When(HttpMethod.Get, $"{domain}/.well-known/change-password") + .RespondWith(HttpStatusCode.OK) + .WithContent(new StringContent("Ok")); + + var httpClient = mockedHandler.ToHttpClient(); + mockHttpFactory.CreateClient("ChangePasswordUri").Returns(httpClient); + + var service = new ChangePasswordUriService(mockHttpFactory); + + var result = await service.GetChangePasswordUri(domain); + + Assert.Null(result); + } + + [Theory] + [InlineData("https://example.com")] + public async Task GetChangePasswordUri_WhenChangePasswordUrlNotFound_ReturnsNull(string domain) + { + var mockHttpFactory = Substitute.For(); + var mockedHandler = new MockedHttpMessageHandler(); + + mockedHandler + .When(HttpMethod.Get, $"{domain}/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200") + .RespondWith(HttpStatusCode.NotFound) + .WithContent(new StringContent("Not found")); + + mockedHandler + .When(HttpMethod.Get, $"{domain}/.well-known/change-password") + .RespondWith(HttpStatusCode.NotFound) + .WithContent(new StringContent("Not found")); + + var httpClient = mockedHandler.ToHttpClient(); + mockHttpFactory.CreateClient("ChangePasswordUri").Returns(httpClient); + + var service = new ChangePasswordUriService(mockHttpFactory); + + var result = await service.GetChangePasswordUri(domain); + + Assert.Null(result); + } + + [Theory] + [InlineData("")] + public async Task GetChangePasswordUri_WhenDomainIsNullOrEmpty_ReturnsNull(string domain) + { + var mockHttpFactory = Substitute.For(); + var service = new ChangePasswordUriService(mockHttpFactory); + + var result = await service.GetChangePasswordUri(domain); + + Assert.Null(result); + } +} From b63e27249087c1c31fd564a20e2c8a88acde4f69 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:28:03 -0500 Subject: [PATCH 168/326] [PM-24551] remove feature flag code for pm-199566-update-msp-to-charge-automatically (#6188) * [PM-24551] remove feature flag code * undoing constructor refactors * reverting changes the refactor made --- .../Controllers/ProvidersController.cs | 40 +++++-------- .../AdminConsole/Views/Providers/Edit.cshtml | 15 +++-- .../PaymentMethodAttachedHandler.cs | 59 +------------------ src/Core/Constants.cs | 1 - 4 files changed, 23 insertions(+), 92 deletions(-) diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index c0c138d0bc..9344179a77 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -7,7 +7,6 @@ using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; using Bit.Admin.Services; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -39,28 +38,26 @@ namespace Bit.Admin.AdminConsole.Controllers; [SelfHosted(NotSelfHostedOnly = true)] public class ProvidersController : Controller { + private readonly string _stripeUrl; + private readonly string _braintreeMerchantUrl; + private readonly string _braintreeMerchantId; private readonly IOrganizationRepository _organizationRepository; private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand; private readonly IProviderRepository _providerRepository; private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository; + private readonly IProviderService _providerService; private readonly GlobalSettings _globalSettings; private readonly IApplicationCacheService _applicationCacheService; - private readonly IProviderService _providerService; private readonly ICreateProviderCommand _createProviderCommand; - private readonly IFeatureService _featureService; private readonly IProviderPlanRepository _providerPlanRepository; private readonly IProviderBillingService _providerBillingService; private readonly IPricingClient _pricingClient; private readonly IStripeAdapter _stripeAdapter; private readonly IAccessControlService _accessControlService; private readonly ISubscriberService _subscriberService; - private readonly string _stripeUrl; - private readonly string _braintreeMerchantUrl; - private readonly string _braintreeMerchantId; - public ProvidersController( - IOrganizationRepository organizationRepository, + public ProvidersController(IOrganizationRepository organizationRepository, IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand, IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, @@ -69,7 +66,6 @@ public class ProvidersController : Controller GlobalSettings globalSettings, IApplicationCacheService applicationCacheService, ICreateProviderCommand createProviderCommand, - IFeatureService featureService, IProviderPlanRepository providerPlanRepository, IProviderBillingService providerBillingService, IWebHostEnvironment webHostEnvironment, @@ -87,15 +83,14 @@ public class ProvidersController : Controller _globalSettings = globalSettings; _applicationCacheService = applicationCacheService; _createProviderCommand = createProviderCommand; - _featureService = featureService; _providerPlanRepository = providerPlanRepository; _providerBillingService = providerBillingService; _pricingClient = pricingClient; _stripeAdapter = stripeAdapter; + _accessControlService = accessControlService; _stripeUrl = webHostEnvironment.GetStripeUrl(); _braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl(); _braintreeMerchantId = globalSettings.Braintree.MerchantId; - _accessControlService = accessControlService; _subscriberService = subscriberService; } @@ -344,21 +339,17 @@ public class ProvidersController : Controller ]); await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand); - if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically)) + var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId); + if (model.PayByInvoice != customer.ApprovedToPayByInvoice()) { - var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId); - - if (model.PayByInvoice != customer.ApprovedToPayByInvoice()) + var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0"; + await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { - var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0"; - await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions + Metadata = new Dictionary { - Metadata = new Dictionary - { - [StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice - } - }); - } + [StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice + } + }); } break; case ProviderType.BusinessUnit: @@ -403,8 +394,7 @@ public class ProvidersController : Controller } var providerPlans = await _providerPlanRepository.GetByProviderId(id); - var payByInvoice = _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && - ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false); + var payByInvoice = ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false); return new ProviderEditModel( provider, users, providerOrganizations, diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index ca4fa70ab5..e450322e97 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -1,12 +1,11 @@ -@using Bit.Admin.Enums; -@using Bit.Core +@inject IAccessControlService AccessControlService + +@using Bit.Admin.Enums +@using Bit.Admin.Services @using Bit.Core.AdminConsole.Enums.Provider @using Bit.Core.Billing.Enums @using Bit.Core.Billing.Extensions -@using Microsoft.AspNetCore.Mvc.TagHelpers -@inject Bit.Admin.Services.IAccessControlService AccessControlService -@inject Bit.Core.Services.IFeatureService FeatureService - +@using Bit.Core.Enums @model ProviderEditModel @{ ViewData["Title"] = "Provider: " + Model.Provider.DisplayName(); @@ -114,7 +113,7 @@
-
@@ -144,7 +143,7 @@
- @if (FeatureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable()) + @if (Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable()) {
diff --git a/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs b/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs index ee5a50cc98..548a41879c 100644 --- a/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs +++ b/src/Billing/Services/Implementations/PaymentMethodAttachedHandler.cs @@ -2,12 +2,10 @@ #nullable disable using Bit.Billing.Constants; -using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; -using Bit.Core.Services; using Stripe; using Event = Stripe.Event; @@ -19,41 +17,22 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler private readonly IStripeEventService _stripeEventService; private readonly IStripeFacade _stripeFacade; private readonly IStripeEventUtilityService _stripeEventUtilityService; - private readonly IFeatureService _featureService; private readonly IProviderRepository _providerRepository; - public PaymentMethodAttachedHandler( - ILogger logger, + public PaymentMethodAttachedHandler(ILogger logger, IStripeEventService stripeEventService, IStripeFacade stripeFacade, IStripeEventUtilityService stripeEventUtilityService, - IFeatureService featureService, IProviderRepository providerRepository) { _logger = logger; _stripeEventService = stripeEventService; _stripeFacade = stripeFacade; _stripeEventUtilityService = stripeEventUtilityService; - _featureService = featureService; _providerRepository = providerRepository; } public async Task HandleAsync(Event parsedEvent) - { - var updateMSPToChargeAutomatically = - _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically); - - if (updateMSPToChargeAutomatically) - { - await HandleVNextAsync(parsedEvent); - } - else - { - await HandleVCurrentAsync(parsedEvent); - } - } - - private async Task HandleVNextAsync(Event parsedEvent) { var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent, true, ["customer.subscriptions.data.latest_invoice"]); @@ -136,42 +115,6 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler } } - private async Task HandleVCurrentAsync(Event parsedEvent) - { - var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent); - if (paymentMethod is null) - { - _logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null"); - return; - } - - var subscriptionListOptions = new SubscriptionListOptions - { - Customer = paymentMethod.CustomerId, - Status = StripeSubscriptionStatus.Unpaid, - Expand = ["data.latest_invoice"] - }; - - StripeList unpaidSubscriptions; - try - { - unpaidSubscriptions = await _stripeFacade.ListSubscriptions(subscriptionListOptions); - } - catch (Exception e) - { - _logger.LogError(e, - "Attempted to get unpaid invoices for customer {CustomerId} but encountered an error while calling Stripe", - paymentMethod.CustomerId); - - return; - } - - foreach (var unpaidSubscription in unpaidSubscriptions) - { - await AttemptToPayOpenSubscriptionAsync(unpaidSubscription); - } - } - private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription) { var latestInvoice = unpaidSubscription.LatestInvoice; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7f55a0710d..2fbf7caffd 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -152,7 +152,6 @@ public static class FeatureFlagKeys public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; - public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; From 7a63ae6315f46f08e9a34618281bcfd27cbab6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:38:01 +0100 Subject: [PATCH 169/326] =?UTF-8?q?[PM-22838]=C2=A0Add=20hyperlink=20to=20?= =?UTF-8?q?provider=20name=20in=20Admin=20Panel=20organization=20details?= =?UTF-8?q?=20(#6243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Organizations/_ProviderInformation.cshtml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Admin/AdminConsole/Views/Organizations/_ProviderInformation.cshtml b/src/Admin/AdminConsole/Views/Organizations/_ProviderInformation.cshtml index 03ecad452d..f6e068e0ae 100644 --- a/src/Admin/AdminConsole/Views/Organizations/_ProviderInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/_ProviderInformation.cshtml @@ -2,7 +2,9 @@ @model Bit.Core.AdminConsole.Entities.Provider.Provider
Provider Name
-
@Model.DisplayName()
+
+ @Model.DisplayName() +
Provider Type
@(Model.Type.GetDisplayAttribute()?.GetName())
From e5159a3ba2c40173104198fd621cdc23386854cf Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:30:37 -0400 Subject: [PATCH 170/326] [PM-19659] Clean up Notifications code (#6244) * Move PushType to Platform Folder - Move the PushType next to the rest of push notification code - Specifically exclude it from needing Platform code review - Add tests establishing rules Platform has for usage of this enum, making it safe to have no owner * Move NotificationHub code into Platform/Push directory * Update NotificationHub namespace imports * Add attribute for storing push type metadata * Rename Push Engines to have PushEngine suffix * Move Push Registration items to their own directory * Push code move * Add expected usage comment * Add Push feature registration method - Make method able to be called multipes times with no ill effects * Add Push Registration service entrypoint and tests * Use new service entrypoints * Test changes --- .github/CODEOWNERS | 2 + src/Api/Models/Request/DeviceRequestModels.cs | 2 +- .../Push/Controllers/PushController.cs | 2 +- src/Core/Enums/PushType.cs | 35 ---- src/Core/Models/PushNotification.cs | 6 +- .../AzureQueuePushEngine.cs} | 10 +- .../MultiServicePushNotificationService.cs | 8 +- .../NoopPushEngine.cs} | 5 +- .../NotificationsApiPushEngine.cs} | 12 +- .../RelayPushEngine.cs} | 9 +- .../Push/{Services => }/IPushEngine.cs | 5 +- .../IPushNotificationService.cs | 52 ++++- .../Push/{Services => }/IPushRelayer.cs | 4 +- .../INotificationHubClientProxy.cs | 4 +- .../NotificationHub/INotificationHubPool.cs | 4 +- .../NotificationHubClientProxy.cs | 4 +- .../NotificationHubConnection.cs | 4 +- .../NotificationHub/NotificationHubPool.cs | 4 +- .../NotificationHubPushEngine.cs} | 15 +- .../Push/NotificationInfoAttribute.cs | 44 ++++ .../Push/{Services => }/PushNotification.cs | 3 + .../Push/PushServiceCollectionExtensions.cs | 82 ++++++++ src/Core/Platform/Push/PushType.cs | 93 ++++++++ .../IPushRegistrationService.cs | 8 +- .../NoopPushRegistrationService.cs | 8 +- .../NotificationHubPushRegistrationService.cs | 5 +- .../PushRegistration}/PushRegistrationData.cs | 4 +- ...RegistrationServiceCollectionExtensions.cs | 54 +++++ .../RelayPushRegistrationService.cs | 8 +- src/Core/Services/IDeviceService.cs | 2 +- .../Services/Implementations/DeviceService.cs | 2 +- .../Utilities/ServiceCollectionExtensions.cs | 46 +--- .../Controllers/PushControllerTests.cs | 2 +- .../Push/Controllers/PushControllerTests.cs | 2 +- .../AzureQueuePushEngineTests.cs} | 11 +- .../NotificationsApiPushEngineTests.cs} | 12 +- .../{Services => Engines}/PushTestBase.cs | 3 + .../RelayPushEngineTests.cs} | 7 +- ...ultiServicePushNotificationServiceTests.cs | 57 +++++ .../NotificationHubConnectionTests.cs | 4 +- .../NotificationHubPoolTests.cs | 4 +- .../NotificationHubProxyTests.cs | 4 +- .../NotificationHubPushEngineTests.cs} | 14 +- .../PushServiceCollectionExtensionsTests.cs | 198 ++++++++++++++++++ test/Core.Test/Platform/Push/PushTypeTests.cs | 64 ++++++ ...ultiServicePushNotificationServiceTests.cs | 8 - ...ficationHubPushRegistrationServiceTests.cs | 4 +- ...trationServiceCollectionExtensionsTests.cs | 108 ++++++++++ .../RelayPushRegistrationServiceTests.cs | 2 +- test/Core.Test/Services/DeviceServiceTests.cs | 2 +- .../Factories/WebApplicationFactoryBase.cs | 2 +- 51 files changed, 849 insertions(+), 205 deletions(-) delete mode 100644 src/Core/Enums/PushType.cs rename src/Core/Platform/Push/{Services/AzureQueuePushNotificationService.cs => Engines/AzureQueuePushEngine.cs} (91%) rename src/Core/Platform/Push/{Services => Engines}/MultiServicePushNotificationService.cs (91%) rename src/Core/Platform/Push/{Services/NoopPushNotificationService.cs => Engines/NoopPushEngine.cs} (75%) rename src/Core/Platform/Push/{Services/NotificationsApiPushNotificationService.cs => Engines/NotificationsApiPushEngine.cs} (87%) rename src/Core/Platform/Push/{Services/RelayPushNotificationService.cs => Engines/RelayPushEngine.cs} (94%) rename src/Core/Platform/Push/{Services => }/IPushEngine.cs (76%) rename src/Core/Platform/Push/{Services => }/IPushNotificationService.cs (82%) rename src/Core/Platform/Push/{Services => }/IPushRelayer.cs (97%) rename src/Core/{ => Platform/Push}/NotificationHub/INotificationHubClientProxy.cs (82%) rename src/Core/{ => Platform/Push}/NotificationHub/INotificationHubPool.cs (81%) rename src/Core/{ => Platform/Push}/NotificationHub/NotificationHubClientProxy.cs (94%) rename src/Core/{ => Platform/Push}/NotificationHub/NotificationHubConnection.cs (99%) rename src/Core/{ => Platform/Push}/NotificationHub/NotificationHubPool.cs (98%) rename src/Core/{NotificationHub/NotificationHubPushNotificationService.cs => Platform/Push/NotificationHub/NotificationHubPushEngine.cs} (95%) create mode 100644 src/Core/Platform/Push/NotificationInfoAttribute.cs rename src/Core/Platform/Push/{Services => }/PushNotification.cs (96%) create mode 100644 src/Core/Platform/Push/PushServiceCollectionExtensions.cs create mode 100644 src/Core/Platform/Push/PushType.cs rename src/Core/Platform/{Push/Services => PushRegistration}/IPushRegistrationService.cs (79%) rename src/Core/Platform/{Push/Services => PushRegistration}/NoopPushRegistrationService.cs (86%) rename src/Core/{NotificationHub => Platform/PushRegistration}/NotificationHubPushRegistrationService.cs (99%) rename src/Core/{NotificationHub => Platform/PushRegistration}/PushRegistrationData.cs (92%) create mode 100644 src/Core/Platform/PushRegistration/PushRegistrationServiceCollectionExtensions.cs rename src/Core/Platform/{Push/Services => PushRegistration}/RelayPushRegistrationService.cs (95%) rename test/Core.Test/Platform/Push/{Services/AzureQueuePushNotificationServiceTests.cs => Engines/AzureQueuePushEngineTests.cs} (98%) rename test/Core.Test/Platform/Push/{Services/NotificationsApiPushNotificationServiceTests.cs => Engines/NotificationsApiPushEngineTests.cs} (97%) rename test/Core.Test/Platform/Push/{Services => Engines}/PushTestBase.cs (99%) rename test/Core.Test/Platform/Push/{Services/RelayPushNotificationServiceTests.cs => Engines/RelayPushEngineTests.cs} (98%) create mode 100644 test/Core.Test/Platform/Push/MultiServicePushNotificationServiceTests.cs rename test/Core.Test/{ => Platform/Push}/NotificationHub/NotificationHubConnectionTests.cs (98%) rename test/Core.Test/{ => Platform/Push}/NotificationHub/NotificationHubPoolTests.cs (98%) rename test/Core.Test/{ => Platform/Push}/NotificationHub/NotificationHubProxyTests.cs (93%) rename test/Core.Test/{NotificationHub/NotificationHubPushNotificationServiceTests.cs => Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs} (98%) create mode 100644 test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs create mode 100644 test/Core.Test/Platform/Push/PushTypeTests.cs delete mode 100644 test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs rename test/Core.Test/{NotificationHub => Platform/PushRegistration}/NotificationHubPushRegistrationServiceTests.cs (99%) create mode 100644 test/Core.Test/Platform/PushRegistration/PushRegistrationServiceCollectionExtensionsTests.cs rename test/Core.Test/Platform/{Push/Services => PushRegistration}/RelayPushRegistrationServiceTests.cs (95%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 88cfc71256..2f1c5f18fb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -92,6 +92,8 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev **/.dockerignore @bitwarden/team-platform-dev **/Dockerfile @bitwarden/team-platform-dev **/entrypoint.sh @bitwarden/team-platform-dev +# The PushType enum is expected to be editted by anyone without need for Platform review +src/Core/Platform/Push/PushType.cs # Multiple owners - DO NOT REMOVE (BRE) **/packages.lock.json diff --git a/src/Api/Models/Request/DeviceRequestModels.cs b/src/Api/Models/Request/DeviceRequestModels.cs index 397d4e27df..11600a0195 100644 --- a/src/Api/Models/Request/DeviceRequestModels.cs +++ b/src/Api/Models/Request/DeviceRequestModels.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.NotificationHub; +using Bit.Core.Platform.PushRegistration; using Bit.Core.Utilities; namespace Bit.Api.Models.Request; diff --git a/src/Api/Platform/Push/Controllers/PushController.cs b/src/Api/Platform/Push/Controllers/PushController.cs index 88aec18be3..14c0a20636 100644 --- a/src/Api/Platform/Push/Controllers/PushController.cs +++ b/src/Api/Platform/Push/Controllers/PushController.cs @@ -6,9 +6,9 @@ using System.Text.Json; using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Models.Api; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; diff --git a/src/Core/Enums/PushType.cs b/src/Core/Enums/PushType.cs deleted file mode 100644 index 07c40f94a2..0000000000 --- a/src/Core/Enums/PushType.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Bit.Core.Enums; - -public enum PushType : byte -{ - SyncCipherUpdate = 0, - SyncCipherCreate = 1, - SyncLoginDelete = 2, - SyncFolderDelete = 3, - SyncCiphers = 4, - - SyncVault = 5, - SyncOrgKeys = 6, - SyncFolderCreate = 7, - SyncFolderUpdate = 8, - SyncCipherDelete = 9, - SyncSettings = 10, - - LogOut = 11, - - SyncSendCreate = 12, - SyncSendUpdate = 13, - SyncSendDelete = 14, - - AuthRequest = 15, - AuthRequestResponse = 16, - - SyncOrganizations = 17, - SyncOrganizationStatusChanged = 18, - SyncOrganizationCollectionSettingChanged = 19, - - Notification = 20, - NotificationStatus = 21, - - RefreshSecurityTasks = 22 -} diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index 83c6f577d4..e235d05b13 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -1,9 +1,11 @@ -#nullable enable -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.NotificationCenter.Enums; namespace Bit.Core.Models; +// New push notification payload models should not be defined in this file +// they should instead be defined in file owned by your team. + public class PushNotificationData { public PushNotificationData(PushType type, T payload, string? contextId) diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Engines/AzureQueuePushEngine.cs similarity index 91% rename from src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs rename to src/Core/Platform/Push/Engines/AzureQueuePushEngine.cs index 94a20f1971..e8c8790c64 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Engines/AzureQueuePushEngine.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Text.Json; +using System.Text.Json; using Azure.Storage.Queues; using Bit.Core.Context; using Bit.Core.Enums; @@ -13,17 +12,16 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push.Internal; -public class AzureQueuePushNotificationService : IPushEngine +public class AzureQueuePushEngine : IPushEngine { private readonly QueueClient _queueClient; private readonly IHttpContextAccessor _httpContextAccessor; - public AzureQueuePushNotificationService( + public AzureQueuePushEngine( [FromKeyedServices("notifications")] QueueClient queueClient, IHttpContextAccessor httpContextAccessor, IGlobalSettings globalSettings, - ILogger logger, - TimeProvider timeProvider) + ILogger logger) { _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; diff --git a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs b/src/Core/Platform/Push/Engines/MultiServicePushNotificationService.cs similarity index 91% rename from src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs rename to src/Core/Platform/Push/Engines/MultiServicePushNotificationService.cs index 404b153fa3..1dbd2c83e5 100644 --- a/src/Core/Platform/Push/Services/MultiServicePushNotificationService.cs +++ b/src/Core/Platform/Push/Engines/MultiServicePushNotificationService.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Microsoft.Extensions.Logging; @@ -8,7 +7,7 @@ namespace Bit.Core.Platform.Push.Internal; public class MultiServicePushNotificationService : IPushNotificationService { - private readonly IEnumerable _services; + private readonly IPushEngine[] _services; public Guid InstallationId { get; } @@ -22,7 +21,8 @@ public class MultiServicePushNotificationService : IPushNotificationService GlobalSettings globalSettings, TimeProvider timeProvider) { - _services = services; + // Filter out any NoopPushEngine's + _services = [.. services.Where(engine => engine is not NoopPushEngine)]; Logger = logger; Logger.LogInformation("Hub services: {Services}", _services.Count()); diff --git a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs b/src/Core/Platform/Push/Engines/NoopPushEngine.cs similarity index 75% rename from src/Core/Platform/Push/Services/NoopPushNotificationService.cs rename to src/Core/Platform/Push/Engines/NoopPushEngine.cs index e6f71de006..029d6dd556 100644 --- a/src/Core/Platform/Push/Services/NoopPushNotificationService.cs +++ b/src/Core/Platform/Push/Engines/NoopPushEngine.cs @@ -1,10 +1,9 @@ -#nullable enable -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Vault.Entities; namespace Bit.Core.Platform.Push.Internal; -internal class NoopPushNotificationService : IPushEngine +internal class NoopPushEngine : IPushEngine { public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds) => Task.CompletedTask; diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Engines/NotificationsApiPushEngine.cs similarity index 87% rename from src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs rename to src/Core/Platform/Push/Engines/NotificationsApiPushEngine.cs index 5e0d584ba8..add53278ff 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Engines/NotificationsApiPushEngine.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Services; @@ -8,23 +7,22 @@ using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -// This service is not in the `Internal` namespace because it has direct external references. -namespace Bit.Core.Platform.Push; +namespace Bit.Core.Platform.Push.Internal; /// /// Sends non-mobile push notifications to the Azure Queue Api, later received by Notifications Api. /// Used by Cloud-Hosted environments. /// Received by AzureQueueHostedService message receiver in Notifications project. /// -public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushEngine +public class NotificationsApiPushEngine : BaseIdentityClientService, IPushEngine { private readonly IHttpContextAccessor _httpContextAccessor; - public NotificationsApiPushNotificationService( + public NotificationsApiPushEngine( IHttpClientFactory httpFactory, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger) : base( httpFactory, globalSettings.BaseServiceUri.InternalNotifications, diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Engines/RelayPushEngine.cs similarity index 94% rename from src/Core/Platform/Push/Services/RelayPushNotificationService.cs rename to src/Core/Platform/Push/Engines/RelayPushEngine.cs index 9f2289b864..66b0229315 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Engines/RelayPushEngine.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.IdentityServer; using Bit.Core.Models; @@ -19,18 +18,18 @@ namespace Bit.Core.Platform.Push.Internal; /// Used by Self-Hosted environments. /// Received by PushController endpoint in Api project. ///
-public class RelayPushNotificationService : BaseIdentityClientService, IPushEngine +public class RelayPushEngine : BaseIdentityClientService, IPushEngine { private readonly IDeviceRepository _deviceRepository; private readonly IHttpContextAccessor _httpContextAccessor; - public RelayPushNotificationService( + public RelayPushEngine( IHttpClientFactory httpFactory, IDeviceRepository deviceRepository, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger) : base( httpFactory, globalSettings.PushRelayBaseUri, diff --git a/src/Core/Platform/Push/Services/IPushEngine.cs b/src/Core/Platform/Push/IPushEngine.cs similarity index 76% rename from src/Core/Platform/Push/Services/IPushEngine.cs rename to src/Core/Platform/Push/IPushEngine.cs index bde4ddaf4b..ca00dae3ad 100644 --- a/src/Core/Platform/Push/Services/IPushEngine.cs +++ b/src/Core/Platform/Push/IPushEngine.cs @@ -1,8 +1,7 @@ -#nullable enable -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Vault.Entities; -namespace Bit.Core.Platform.Push; +namespace Bit.Core.Platform.Push.Internal; public interface IPushEngine { diff --git a/src/Core/Platform/Push/Services/IPushNotificationService.cs b/src/Core/Platform/Push/IPushNotificationService.cs similarity index 82% rename from src/Core/Platform/Push/Services/IPushNotificationService.cs rename to src/Core/Platform/Push/IPushNotificationService.cs index c6da6cf6b7..339ce5a917 100644 --- a/src/Core/Platform/Push/Services/IPushNotificationService.cs +++ b/src/Core/Platform/Push/IPushNotificationService.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Enums; using Bit.Core.Models; @@ -10,10 +9,27 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Platform.Push; +/// +/// Used to Push notifications to end-user devices. +/// +/// +/// New notifications should not be wired up inside this service. You may either directly call the +/// method in your service to send your notification or if you want your notification +/// sent by other teams you can make an extension method on this service with a well typed definition +/// of your notification. You may also make your own service that injects this and exposes methods for each of +/// your notifications. +/// public interface IPushNotificationService { + private const string ServiceDeprecation = "Do not use the services exposed here, instead use your own services injected in your service."; + + [Obsolete(ServiceDeprecation, DiagnosticId = "BWP0001")] Guid InstallationId { get; } + + [Obsolete(ServiceDeprecation, DiagnosticId = "BWP0001")] TimeProvider TimeProvider { get; } + + [Obsolete(ServiceDeprecation, DiagnosticId = "BWP0001")] ILogger Logger { get; } #region Legacy method, to be removed soon. @@ -80,7 +96,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -94,7 +112,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -108,7 +128,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -122,7 +144,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -136,7 +160,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -150,7 +176,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = excludeCurrentContextFromPush, }); @@ -231,7 +259,9 @@ public interface IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, +#pragma warning disable BWP0001 // Type or member is obsolete InstallationId = notification.Global ? InstallationId : null, +#pragma warning restore BWP0001 // Type or member is obsolete TaskId = notification.TaskId, Title = notification.Title, Body = notification.Body, @@ -246,7 +276,9 @@ public interface IPushNotificationService { // TODO: Think about this a bit more target = NotificationTarget.Installation; +#pragma warning disable BWP0001 // Type or member is obsolete targetId = InstallationId; +#pragma warning restore BWP0001 // Type or member is obsolete } else if (notification.UserId.HasValue) { @@ -260,7 +292,9 @@ public interface IPushNotificationService } else { +#pragma warning disable BWP0001 // Type or member is obsolete Logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id); +#pragma warning restore BWP0001 // Type or member is obsolete return Task.CompletedTask; } @@ -285,7 +319,9 @@ public interface IPushNotificationService ClientType = notification.ClientType, UserId = notification.UserId, OrganizationId = notification.OrganizationId, +#pragma warning disable BWP0001 // Type or member is obsolete InstallationId = notification.Global ? InstallationId : null, +#pragma warning restore BWP0001 // Type or member is obsolete TaskId = notification.TaskId, Title = notification.Title, Body = notification.Body, @@ -302,7 +338,9 @@ public interface IPushNotificationService { // TODO: Think about this a bit more target = NotificationTarget.Installation; +#pragma warning disable BWP0001 // Type or member is obsolete targetId = InstallationId; +#pragma warning restore BWP0001 // Type or member is obsolete } else if (notification.UserId.HasValue) { @@ -316,7 +354,9 @@ public interface IPushNotificationService } else { +#pragma warning disable BWP0001 // Type or member is obsolete Logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id); +#pragma warning restore BWP0001 // Type or member is obsolete return Task.CompletedTask; } @@ -398,7 +438,9 @@ public interface IPushNotificationService Payload = new UserPushNotification { UserId = userId, +#pragma warning disable BWP0001 // Type or member is obsolete Date = TimeProvider.GetUtcNow().UtcDateTime, +#pragma warning restore BWP0001 // Type or member is obsolete }, ExcludeCurrentContext = false, }); @@ -406,6 +448,12 @@ public interface IPushNotificationService Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable? collectionIds); + /// + /// Pushes a notification to devices based on the settings given to us in . + /// + /// The type of the payload to be sent along with the notification. + /// + /// A task that is NOT guarunteed to have sent the notification by the time the task resolves. Task PushAsync(PushNotification pushNotification) where T : class; } diff --git a/src/Core/Platform/Push/Services/IPushRelayer.cs b/src/Core/Platform/Push/IPushRelayer.cs similarity index 97% rename from src/Core/Platform/Push/Services/IPushRelayer.cs rename to src/Core/Platform/Push/IPushRelayer.cs index fde0a521f3..1fb75e0dfc 100644 --- a/src/Core/Platform/Push/Services/IPushRelayer.cs +++ b/src/Core/Platform/Push/IPushRelayer.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Enums; namespace Bit.Core.Platform.Push.Internal; diff --git a/src/Core/NotificationHub/INotificationHubClientProxy.cs b/src/Core/Platform/Push/NotificationHub/INotificationHubClientProxy.cs similarity index 82% rename from src/Core/NotificationHub/INotificationHubClientProxy.cs rename to src/Core/Platform/Push/NotificationHub/INotificationHubClientProxy.cs index 78eb0206d6..8b765d209b 100644 --- a/src/Core/NotificationHub/INotificationHubClientProxy.cs +++ b/src/Core/Platform/Push/NotificationHub/INotificationHubClientProxy.cs @@ -1,8 +1,6 @@ using Microsoft.Azure.NotificationHubs; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; public interface INotificationHubProxy { diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/Platform/Push/NotificationHub/INotificationHubPool.cs similarity index 81% rename from src/Core/NotificationHub/INotificationHubPool.cs rename to src/Core/Platform/Push/NotificationHub/INotificationHubPool.cs index 25a31d62f4..3d5767623b 100644 --- a/src/Core/NotificationHub/INotificationHubPool.cs +++ b/src/Core/Platform/Push/NotificationHub/INotificationHubPool.cs @@ -1,8 +1,6 @@ using Microsoft.Azure.NotificationHubs; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; public interface INotificationHubPool { diff --git a/src/Core/NotificationHub/NotificationHubClientProxy.cs b/src/Core/Platform/Push/NotificationHub/NotificationHubClientProxy.cs similarity index 94% rename from src/Core/NotificationHub/NotificationHubClientProxy.cs rename to src/Core/Platform/Push/NotificationHub/NotificationHubClientProxy.cs index b47069fe21..026f3179d1 100644 --- a/src/Core/NotificationHub/NotificationHubClientProxy.cs +++ b/src/Core/Platform/Push/NotificationHub/NotificationHubClientProxy.cs @@ -1,8 +1,6 @@ using Microsoft.Azure.NotificationHubs; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; public class NotificationHubClientProxy : INotificationHubProxy { diff --git a/src/Core/NotificationHub/NotificationHubConnection.cs b/src/Core/Platform/Push/NotificationHub/NotificationHubConnection.cs similarity index 99% rename from src/Core/NotificationHub/NotificationHubConnection.cs rename to src/Core/Platform/Push/NotificationHub/NotificationHubConnection.cs index a61f2ded8f..22c1668506 100644 --- a/src/Core/NotificationHub/NotificationHubConnection.cs +++ b/src/Core/Platform/Push/NotificationHub/NotificationHubConnection.cs @@ -6,9 +6,7 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; public class NotificationHubConnection { diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/Platform/Push/NotificationHub/NotificationHubPool.cs similarity index 98% rename from src/Core/NotificationHub/NotificationHubPool.cs rename to src/Core/Platform/Push/NotificationHub/NotificationHubPool.cs index 38192c11fc..c3dc47809f 100644 --- a/src/Core/NotificationHub/NotificationHubPool.cs +++ b/src/Core/Platform/Push/NotificationHub/NotificationHubPool.cs @@ -3,9 +3,7 @@ using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; using Microsoft.Extensions.Logging; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; public class NotificationHubPool : INotificationHubPool { diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/Platform/Push/NotificationHub/NotificationHubPushEngine.cs similarity index 95% rename from src/Core/NotificationHub/NotificationHubPushNotificationService.cs rename to src/Core/Platform/Push/NotificationHub/NotificationHubPushEngine.cs index 81ec82a25d..1d1eb2ef70 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/Platform/Push/NotificationHub/NotificationHubPushEngine.cs @@ -1,28 +1,23 @@ -#nullable enable -using System.Text.Json; +using System.Text.Json; using System.Text.RegularExpressions; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Data; -using Bit.Core.Platform.Push; -using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.Push.Internal; /// /// Sends mobile push notifications to the Azure Notification Hub. /// Used by Cloud-Hosted environments. /// Received by Firebase for Android or APNS for iOS. /// -public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer +public class NotificationHubPushEngine : IPushEngine, IPushRelayer { private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IHttpContextAccessor _httpContextAccessor; @@ -30,11 +25,11 @@ public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; - public NotificationHubPushNotificationService( + public NotificationHubPushEngine( IInstallationDeviceRepository installationDeviceRepository, INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, - ILogger logger, + ILogger logger, IGlobalSettings globalSettings) { _installationDeviceRepository = installationDeviceRepository; diff --git a/src/Core/Platform/Push/NotificationInfoAttribute.cs b/src/Core/Platform/Push/NotificationInfoAttribute.cs new file mode 100644 index 0000000000..ff134f5fda --- /dev/null +++ b/src/Core/Platform/Push/NotificationInfoAttribute.cs @@ -0,0 +1,44 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Platform.Push; + +/// +/// Used to annotate information about a given . +/// +[AttributeUsage(AttributeTargets.Field)] +public class NotificationInfoAttribute : Attribute +{ + // Once upon a time we can feed this information into a C# analyzer to make sure that we validate + // the callsites of IPushNotificationService.PushAsync uses the correct payload type for the notification type + // for now this only exists as forced documentation to teams who create a push type. + + // It's especially on purpose that we allow ourselves to take a type name via just the string, + // this allows teams to make a push type that is only sent with a payload that exists in a separate assembly than + // this one. + + public NotificationInfoAttribute(string team, Type payloadType) + // It should be impossible to reference an unnamed type for an attributes constructor so this assertion should be safe. + : this(team, payloadType.FullName!) + { + Team = team; + } + + public NotificationInfoAttribute(string team, string payloadTypeName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(team); + ArgumentException.ThrowIfNullOrWhiteSpace(payloadTypeName); + + Team = team; + PayloadTypeName = payloadTypeName; + } + + /// + /// The name of the team that owns this . + /// + public string Team { get; } + + /// + /// The fully qualified type name of the payload that should be used when sending a notification of this type. + /// + public string PayloadTypeName { get; } +} diff --git a/src/Core/Platform/Push/Services/PushNotification.cs b/src/Core/Platform/Push/PushNotification.cs similarity index 96% rename from src/Core/Platform/Push/Services/PushNotification.cs rename to src/Core/Platform/Push/PushNotification.cs index e1d3f44cd8..3150b854a4 100644 --- a/src/Core/Platform/Push/Services/PushNotification.cs +++ b/src/Core/Platform/Push/PushNotification.cs @@ -6,6 +6,9 @@ namespace Bit.Core.Platform.Push; /// /// Contains constants for all the available targets for a given notification. /// +/// +/// Please reach out to the Platform team if you need a new target added. +/// public enum NotificationTarget { /// diff --git a/src/Core/Platform/Push/PushServiceCollectionExtensions.cs b/src/Core/Platform/Push/PushServiceCollectionExtensions.cs new file mode 100644 index 0000000000..b54ae64c08 --- /dev/null +++ b/src/Core/Platform/Push/PushServiceCollectionExtensions.cs @@ -0,0 +1,82 @@ +using Azure.Storage.Queues; +using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for adding the Push feature. +/// +public static class PushServiceCollectionExtensions +{ + /// + /// Adds a to the services that can be used to send push notifications to + /// end user devices. This method is safe to be ran multiple time provided does not + /// change between calls. + /// + /// The to add services to. + /// The to use to configure services. + /// The for additional chaining. + public static IServiceCollection AddPush(this IServiceCollection services, GlobalSettings globalSettings) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(globalSettings); + + services.TryAddSingleton(TimeProvider.System); + services.TryAddSingleton(); + + if (globalSettings.SelfHosted) + { + if (globalSettings.Installation.Id == Guid.Empty) + { + throw new InvalidOperationException("Installation Id must be set for self-hosted installations."); + } + + if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && + CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) + { + // TODO: We should really define the HttpClient we will use here + services.AddHttpClient(); + services.AddHttpContextAccessor(); + // We also depend on IDeviceRepository but don't explicitly add it right now. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } + + if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && + CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) + { + // TODO: We should really define the HttpClient we will use here + services.AddHttpClient(); + services.AddHttpContextAccessor(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } + } + else + { + services.TryAddSingleton(); + services.AddHttpContextAccessor(); + + // We also depend on IInstallationDeviceRepository but don't explicitly add it right now. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + services.TryAddSingleton(); + + if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) + { + services.TryAddKeyedSingleton("notifications", static (sp, _) => + { + var gs = sp.GetRequiredService(); + return new QueueClient(gs.Notifications.ConnectionString, "notifications"); + }); + + // We not IHttpContextAccessor will be added above, no need to do it here. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } + } + + return services; + } +} diff --git a/src/Core/Platform/Push/PushType.cs b/src/Core/Platform/Push/PushType.cs new file mode 100644 index 0000000000..7fcb60b4ef --- /dev/null +++ b/src/Core/Platform/Push/PushType.cs @@ -0,0 +1,93 @@ +using Bit.Core.Platform.Push; + +// TODO: This namespace should change to `Bit.Core.Platform.Push` +namespace Bit.Core.Enums; + +/// +/// +/// +/// +/// +/// When adding a new enum member you must annotate it with a +/// this is enforced with a unit test. It is preferred that you do NOT add new usings for the type referenced +/// in . +/// +/// +/// You may and are +/// +/// +public enum PushType : byte +{ + // When adding a new enum member you must annotate it with a NotificationInfoAttribute this is enforced with a unit + // test. It is preferred that you do NOT add new usings for the type referenced for the payload. You are also + // encouraged to define the payload type in your own teams owned code. + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncCipherPushNotification))] + SyncCipherUpdate = 0, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncCipherPushNotification))] + SyncCipherCreate = 1, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncCipherPushNotification))] + SyncLoginDelete = 2, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncFolderPushNotification))] + SyncFolderDelete = 3, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))] + SyncCiphers = 4, + + [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] + SyncVault = 5, + + [NotificationInfo("@bitwarden/team-admin-console-dev", typeof(Models.UserPushNotification))] + SyncOrgKeys = 6, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncFolderPushNotification))] + SyncFolderCreate = 7, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncFolderPushNotification))] + SyncFolderUpdate = 8, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.SyncCipherPushNotification))] + SyncCipherDelete = 9, + + [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] + SyncSettings = 10, + + [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] + LogOut = 11, + + [NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))] + SyncSendCreate = 12, + + [NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))] + SyncSendUpdate = 13, + + [NotificationInfo("@bitwarden/team-tools-dev", typeof(Models.SyncSendPushNotification))] + SyncSendDelete = 14, + + [NotificationInfo("@bitwarden/team-auth-dev", typeof(Models.AuthRequestPushNotification))] + AuthRequest = 15, + + [NotificationInfo("@bitwarden/team-auth-dev", typeof(Models.AuthRequestPushNotification))] + AuthRequestResponse = 16, + + [NotificationInfo("not-specified", typeof(Models.UserPushNotification))] + SyncOrganizations = 17, + + [NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.OrganizationStatusPushNotification))] + SyncOrganizationStatusChanged = 18, + + [NotificationInfo("@bitwarden/team-admin-console-dev", typeof(Models.OrganizationCollectionManagementPushNotification))] + SyncOrganizationCollectionSettingChanged = 19, + + [NotificationInfo("not-specified", typeof(Models.NotificationPushNotification))] + Notification = 20, + + [NotificationInfo("not-specified", typeof(Models.NotificationPushNotification))] + NotificationStatus = 21, + + [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))] + RefreshSecurityTasks = 22, +} diff --git a/src/Core/Platform/Push/Services/IPushRegistrationService.cs b/src/Core/Platform/PushRegistration/IPushRegistrationService.cs similarity index 79% rename from src/Core/Platform/Push/Services/IPushRegistrationService.cs rename to src/Core/Platform/PushRegistration/IPushRegistrationService.cs index 8e34e5e316..d650842f32 100644 --- a/src/Core/Platform/Push/Services/IPushRegistrationService.cs +++ b/src/Core/Platform/PushRegistration/IPushRegistrationService.cs @@ -1,10 +1,10 @@ -#nullable enable - -using Bit.Core.Enums; -using Bit.Core.NotificationHub; +using Bit.Core.Enums; +using Bit.Core.Platform.PushRegistration; +// TODO: Change this namespace to `Bit.Core.Platform.PushRegistration namespace Bit.Core.Platform.Push; + public interface IPushRegistrationService { Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId); diff --git a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs b/src/Core/Platform/PushRegistration/NoopPushRegistrationService.cs similarity index 86% rename from src/Core/Platform/Push/Services/NoopPushRegistrationService.cs rename to src/Core/Platform/PushRegistration/NoopPushRegistrationService.cs index 32efc95ce6..0aebcbf1f3 100644 --- a/src/Core/Platform/Push/Services/NoopPushRegistrationService.cs +++ b/src/Core/Platform/PushRegistration/NoopPushRegistrationService.cs @@ -1,9 +1,7 @@ -#nullable enable +using Bit.Core.Enums; +using Bit.Core.Platform.Push; -using Bit.Core.Enums; -using Bit.Core.NotificationHub; - -namespace Bit.Core.Platform.Push.Internal; +namespace Bit.Core.Platform.PushRegistration.Internal; public class NoopPushRegistrationService : IPushRegistrationService { diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/Platform/PushRegistration/NotificationHubPushRegistrationService.cs similarity index 99% rename from src/Core/NotificationHub/NotificationHubPushRegistrationService.cs rename to src/Core/Platform/PushRegistration/NotificationHubPushRegistrationService.cs index dc494eecd6..ee02e2bdf1 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/Platform/PushRegistration/NotificationHubPushRegistrationService.cs @@ -6,14 +6,13 @@ using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.Azure.NotificationHubs; using Microsoft.Extensions.Logging; -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.PushRegistration.Internal; public class NotificationHubPushRegistrationService : IPushRegistrationService { diff --git a/src/Core/NotificationHub/PushRegistrationData.cs b/src/Core/Platform/PushRegistration/PushRegistrationData.cs similarity index 92% rename from src/Core/NotificationHub/PushRegistrationData.cs rename to src/Core/Platform/PushRegistration/PushRegistrationData.cs index c11ee7be23..844de4e1be 100644 --- a/src/Core/NotificationHub/PushRegistrationData.cs +++ b/src/Core/Platform/PushRegistration/PushRegistrationData.cs @@ -1,6 +1,4 @@ -namespace Bit.Core.NotificationHub; - -#nullable enable +namespace Bit.Core.Platform.PushRegistration; public record struct WebPushRegistrationData { diff --git a/src/Core/Platform/PushRegistration/PushRegistrationServiceCollectionExtensions.cs b/src/Core/Platform/PushRegistration/PushRegistrationServiceCollectionExtensions.cs new file mode 100644 index 0000000000..841902c964 --- /dev/null +++ b/src/Core/Platform/PushRegistration/PushRegistrationServiceCollectionExtensions.cs @@ -0,0 +1,54 @@ +using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration.Internal; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for adding the Push Registration feature. +/// +public static class PushRegistrationServiceCollectionExtensions +{ + /// + /// Adds a to the service collection. + /// + /// The to add services to. + /// The for chaining. + public static IServiceCollection AddPushRegistration(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + // TODO: Should add feature that brings in IInstallationDeviceRepository once that is featurized + + // Register all possible variants under there concrete type. + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.AddHttpClient(); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(static sp => + { + var globalSettings = sp.GetRequiredService(); + + if (globalSettings.SelfHosted) + { + if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && + CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) + { + return sp.GetRequiredService(); + } + + return sp.GetRequiredService(); + } + + return sp.GetRequiredService(); + }); + + return services; + } +} diff --git a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs b/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs similarity index 95% rename from src/Core/Platform/Push/Services/RelayPushRegistrationService.cs rename to src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs index 20e405935b..96a259ecf8 100644 --- a/src/Core/Platform/Push/Services/RelayPushRegistrationService.cs +++ b/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs @@ -1,14 +1,12 @@ -#nullable enable - -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.IdentityServer; using Bit.Core.Models.Api; -using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -namespace Bit.Core.Platform.Push.Internal; +namespace Bit.Core.Platform.PushRegistration.Internal; public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegistrationService { diff --git a/src/Core/Services/IDeviceService.cs b/src/Core/Services/IDeviceService.cs index 78739e081d..ca13047c77 100644 --- a/src/Core/Services/IDeviceService.cs +++ b/src/Core/Services/IDeviceService.cs @@ -1,6 +1,6 @@ using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Entities; -using Bit.Core.NotificationHub; +using Bit.Core.Platform.PushRegistration; namespace Bit.Core.Services; diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 931dfccdec..ea6e77aa8c 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -3,8 +3,8 @@ using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; +using Bit.Core.Platform.PushRegistration; using Bit.Core.Repositories; using Bit.Core.Settings; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 51383d650e..0dd5431dd7 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -6,7 +6,6 @@ using System.Reflection; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; -using Azure.Storage.Queues; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -35,11 +34,10 @@ using Bit.Core.Identity; using Bit.Core.IdentityServer; using Bit.Core.KeyManagement; using Bit.Core.NotificationCenter; -using Bit.Core.NotificationHub; using Bit.Core.OrganizationFeatures; using Bit.Core.Platform; using Bit.Core.Platform.Push; -using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration.Internal; using Bit.Core.Repositories; using Bit.Core.Resources; using Bit.Core.SecretsManager.Repositories; @@ -279,46 +277,8 @@ public static class ServiceCollectionExtensions services.AddSingleton(); } - services.TryAddSingleton(TimeProvider.System); - - services.AddSingleton(); - if (globalSettings.SelfHosted) - { - if (globalSettings.Installation.Id == Guid.Empty) - { - throw new InvalidOperationException("Installation Id must be set for self-hosted installations."); - } - - if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && - CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) - { - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - - if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && - CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) - { - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - } - } - else - { - services.AddSingleton(); - services.AddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.TryAddSingleton(); - if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString)) - { - services.AddKeyedSingleton("notifications", - (_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications")); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - } - } + services.AddPush(globalSettings); + services.AddPushRegistration(); if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString)) { diff --git a/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs index 32d6389616..1f0ecd4835 100644 --- a/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs +++ b/test/Api.IntegrationTest/Platform/Controllers/PushControllerTests.cs @@ -8,8 +8,8 @@ using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Api; using Bit.Core.Models.Data; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Installations; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using NSubstitute; using Xunit; diff --git a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs index d6a26255e9..e74ebc4c03 100644 --- a/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Platform/Push/Controllers/PushControllerTests.cs @@ -4,8 +4,8 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Api; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; +using Bit.Core.Platform.PushRegistration; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs similarity index 98% rename from test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs rename to test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs index b223ef7252..961d7cd770 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs @@ -22,18 +22,18 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Platform.Push.Services; +namespace Bit.Core.Test.Platform.Push.Engines; [QueueClientCustomize] [SutProviderCustomize] -public class AzureQueuePushNotificationServiceTests +public class AzureQueuePushEngineTests { private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3"); private static readonly string _deviceIdentifier = "test_device_identifier"; private readonly FakeTimeProvider _fakeTimeProvider; private readonly Core.Settings.GlobalSettings _globalSettings = new(); - public AzureQueuePushNotificationServiceTests() + public AzureQueuePushEngineTests() { _fakeTimeProvider = new(); _fakeTimeProvider.SetUtcNow(DateTime.UtcNow); @@ -771,12 +771,11 @@ public class AzureQueuePushNotificationServiceTests var globalSettings = new Core.Settings.GlobalSettings(); - var sut = new AzureQueuePushNotificationService( + var sut = new AzureQueuePushEngine( queueClient, httpContextAccessor, globalSettings, - NullLogger.Instance, - _fakeTimeProvider + NullLogger.Instance ); await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id)); diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs similarity index 97% rename from test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs rename to test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs index 5231456d63..c61c2f37d0 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Engines/NotificationsApiPushEngineTests.cs @@ -2,16 +2,16 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.NotificationCenter.Entities; -using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.Extensions.Logging.Abstractions; -namespace Bit.Core.Test.Platform.Push.Services; +namespace Bit.Core.Test.Platform.Push.Engines; -public class NotificationsApiPushNotificationServiceTests : PushTestBase +public class NotificationsApiPushEngineTests : PushTestBase { - public NotificationsApiPushNotificationServiceTests() + public NotificationsApiPushEngineTests() { GlobalSettings.BaseServiceUri.InternalNotifications = "https://localhost:7777"; GlobalSettings.BaseServiceUri.InternalIdentity = "https://localhost:8888"; @@ -21,11 +21,11 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase protected override IPushEngine CreateService() { - return new NotificationsApiPushNotificationService( + return new NotificationsApiPushEngine( HttpClientFactory, GlobalSettings, HttpContextAccessor, - NullLogger.Instance + NullLogger.Instance ); } diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs similarity index 99% rename from test/Core.Test/Platform/Push/Services/PushTestBase.cs rename to test/Core.Test/Platform/Push/Engines/PushTestBase.cs index 3ff09f1064..9097028370 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs @@ -10,6 +10,7 @@ using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Enums; using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -22,6 +23,8 @@ using NSubstitute; using RichardSzalay.MockHttp; using Xunit; +namespace Bit.Core.Test.Platform.Push.Engines; + public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService { public Guid InstallationId { get; } = installationId; diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs similarity index 98% rename from test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs rename to test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs index ddad05eda0..010ad40d13 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Engines/RelayPushEngineTests.cs @@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Entities; using Bit.Core.NotificationCenter.Entities; -using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -15,7 +14,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NSubstitute; -namespace Bit.Core.Test.Platform.Push.Services; +namespace Bit.Core.Test.Platform.Push.Engines; public class RelayPushNotificationServiceTests : PushTestBase { @@ -39,12 +38,12 @@ public class RelayPushNotificationServiceTests : PushTestBase protected override IPushEngine CreateService() { - return new RelayPushNotificationService( + return new RelayPushEngine( HttpClientFactory, _deviceRepository, GlobalSettings, HttpContextAccessor, - NullLogger.Instance + NullLogger.Instance ); } diff --git a/test/Core.Test/Platform/Push/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/MultiServicePushNotificationServiceTests.cs new file mode 100644 index 0000000000..f0143bae51 --- /dev/null +++ b/test/Core.Test/Platform/Push/MultiServicePushNotificationServiceTests.cs @@ -0,0 +1,57 @@ +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Platform.Push; + +public class MultiServicePushNotificationServiceTests +{ + private readonly IPushEngine _fakeEngine1; + private readonly IPushEngine _fakeEngine2; + + private readonly MultiServicePushNotificationService _sut; + + public MultiServicePushNotificationServiceTests() + { + _fakeEngine1 = Substitute.For(); + _fakeEngine2 = Substitute.For(); + + _sut = new MultiServicePushNotificationService( + [_fakeEngine1, _fakeEngine2], + NullLogger.Instance, + new GlobalSettings(), + new FakeTimeProvider() + ); + } + +#if DEBUG // This test requires debug code in the sut to work properly + [Fact] + public async Task PushAsync_CallsAllEngines() + { + var notification = new PushNotification + { + Target = NotificationTarget.User, + TargetId = Guid.NewGuid(), + Type = PushType.AuthRequest, + Payload = new { }, + ExcludeCurrentContext = false, + }; + + await _sut.PushAsync(notification); + + await _fakeEngine1 + .Received(1) + .PushAsync(Arg.Is>(n => ReferenceEquals(n, notification))); + + await _fakeEngine2 + .Received(1) + .PushAsync(Arg.Is>(n => ReferenceEquals(n, notification))); + } + +#endif +} diff --git a/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubConnectionTests.cs similarity index 98% rename from test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs rename to test/Core.Test/Platform/Push/NotificationHub/NotificationHubConnectionTests.cs index fc76e5c1b7..51ba3a10c6 100644 --- a/test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubConnectionTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Settings; using Bit.Core.Utilities; using Xunit; -namespace Bit.Core.Test.NotificationHub; +namespace Bit.Core.Test.Platform.Push.NotificationHub; public class NotificationHubConnectionTests { diff --git a/test/Core.Test/NotificationHub/NotificationHubPoolTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPoolTests.cs similarity index 98% rename from test/Core.Test/NotificationHub/NotificationHubPoolTests.cs rename to test/Core.Test/Platform/Push/NotificationHub/NotificationHubPoolTests.cs index dd9afb867e..5547ab55dd 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPoolTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPoolTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Extensions.Logging; @@ -6,7 +6,7 @@ using NSubstitute; using Xunit; using static Bit.Core.Settings.GlobalSettings; -namespace Bit.Core.Test.NotificationHub; +namespace Bit.Core.Test.Platform.Push.NotificationHub; public class NotificationHubPoolTests { diff --git a/test/Core.Test/NotificationHub/NotificationHubProxyTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubProxyTests.cs similarity index 93% rename from test/Core.Test/NotificationHub/NotificationHubProxyTests.cs rename to test/Core.Test/Platform/Push/NotificationHub/NotificationHubProxyTests.cs index b2e9c4f9f3..846b6e5fc4 100644 --- a/test/Core.Test/NotificationHub/NotificationHubProxyTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubProxyTests.cs @@ -1,11 +1,11 @@ using AutoFixture; -using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push.Internal; using Bit.Test.Common.AutoFixture; using Microsoft.Azure.NotificationHubs; using NSubstitute; using Xunit; -namespace Bit.Core.Test.NotificationHub; +namespace Bit.Core.Test.Platform.Push.NotificationHub; public class NotificationHubProxyTests { diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs similarity index 98% rename from test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs rename to test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs index 54a6f84339..a32b112675 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/NotificationHub/NotificationHubPushEngineTests.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using Bit.Core.Auth.Entities; using Bit.Core.Context; @@ -7,10 +6,11 @@ using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Enums; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Core.Test.Platform.Push.Engines; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture; @@ -22,7 +22,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; -namespace Bit.Core.Test.NotificationHub; +namespace Bit.Core.Test.Platform.Push.NotificationHub; [SutProviderCustomize] [NotificationStatusCustomize] @@ -621,11 +621,11 @@ public class NotificationHubPushNotificationServiceTests fakeTimeProvider.SetUtcNow(_now); - var sut = new NotificationHubPushNotificationService( + var sut = new NotificationHubPushEngine( installationDeviceRepository, notificationHubPool, httpContextAccessor, - NullLogger.Instance, + NullLogger.Instance, globalSettings ); @@ -676,7 +676,7 @@ public class NotificationHubPushNotificationServiceTests }; private static async Task AssertSendTemplateNotificationAsync( - SutProvider sutProvider, PushType type, object payload, string tag) + SutProvider sutProvider, PushType type, object payload, string tag) { await sutProvider.GetDependency() .Received(1) diff --git a/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs b/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..0ab1a91195 --- /dev/null +++ b/test/Core.Test/Platform/Push/PushServiceCollectionExtensionsTests.cs @@ -0,0 +1,198 @@ +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Platform.Push; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Repositories; +using Bit.Core.Repositories.Noop; +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Xunit; + +namespace Bit.Core.Test.Platform.Push; + +public class PushServiceCollectionExtensionsTests +{ + [Fact] + public void AddPush_SelfHosted_NoConfig_NoEngines() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + Assert.Empty(engines); + } + + [Fact] + public void AddPush_SelfHosted_ConfiguredForRelay_RelayEngineAdded() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, + { "GlobalSettings:Installation:Key", "some_key"}, + { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + var engine = Assert.Single(engines); + Assert.IsType(engine); + } + + [Fact] + public void AddPush_SelfHosted_ConfiguredForApi_ApiEngineAdded() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, + { "GlobalSettings:InternalIdentityKey", "some_key"}, + { "GlobalSettings:BaseServiceUri", "https://example.com" }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + var engine = Assert.Single(engines); + Assert.IsType(engine); + } + + [Fact] + public void AddPush_SelfHosted_ConfiguredForRelayAndApi_TwoEnginesAdded() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() }, + { "GlobalSettings:Installation:Key", "some_key"}, + { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, + { "GlobalSettings:InternalIdentityKey", "some_key"}, + { "GlobalSettings:BaseServiceUri", "https://example.com" }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + Assert.Collection( + engines, + e => Assert.IsType(e), + e => Assert.IsType(e) + ); + } + + [Fact] + public void AddPush_Cloud_NoConfig_AddsNotificationHub() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "false" }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + var engine = Assert.Single(engines); + Assert.IsType(engine); + } + + [Fact] + public void AddPush_Cloud_HasNotificationConnectionString_TwoEngines() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "false" }, + { "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" }, + }); + + _ = services.GetRequiredService(); + var engines = services.GetServices(); + + Assert.Collection( + engines, + e => Assert.IsType(e), + e => Assert.IsType(e) + ); + } + + [Fact] + public void AddPush_Cloud_CalledTwice_DoesNotAddServicesTwice() + { + var services = new ServiceCollection(); + + var config = new Dictionary + { + { "GlobalSettings:SelfHosted", "false" }, + { "GlobalSettings:Notifications:ConnectionString", "UseDevelopmentStorage=true" }, + }; + + AddServices(services, config); + + var initialCount = services.Count; + + // Add services again + AddServices(services, config); + + Assert.Equal(initialCount, services.Count); + } + + private static ServiceProvider Build(Dictionary initialData) + { + var services = new ServiceCollection(); + + AddServices(services, initialData); + + return services.BuildServiceProvider(); + } + + private static void AddServices(IServiceCollection services, Dictionary initialData) + { + // A minimal service collection is always expected to have logging, config, and global settings + // pre-registered. + + services.AddLogging(); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(initialData) + .Build(); + + services.TryAddSingleton(config); + var globalSettings = new GlobalSettings(); + config.GetSection("GlobalSettings").Bind(globalSettings); + + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(globalSettings); + + // Temporary until AddPush can add it themselves directly. + services.TryAddSingleton(); + + // Temporary until AddPush can add it themselves directly. + services.TryAddSingleton(); + + services.AddPush(globalSettings); + } + + private class StubDeviceRepository : IDeviceRepository + { + public Task ClearPushTokenAsync(Guid id) => throw new NotImplementedException(); + public Task CreateAsync(Device obj) => throw new NotImplementedException(); + public Task DeleteAsync(Device obj) => throw new NotImplementedException(); + public Task GetByIdAsync(Guid id, Guid userId) => throw new NotImplementedException(); + public Task GetByIdAsync(Guid id) => throw new NotImplementedException(); + public Task GetByIdentifierAsync(string identifier) => throw new NotImplementedException(); + public Task GetByIdentifierAsync(string identifier, Guid userId) => throw new NotImplementedException(); + public Task> GetManyByUserIdAsync(Guid userId) => throw new NotImplementedException(); + public Task> GetManyByUserIdWithDeviceAuth(Guid userId) => throw new NotImplementedException(); + public Task ReplaceAsync(Device obj) => throw new NotImplementedException(); + public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable devices) => throw new NotImplementedException(); + public Task UpsertAsync(Device obj) => throw new NotImplementedException(); + } +} diff --git a/test/Core.Test/Platform/Push/PushTypeTests.cs b/test/Core.Test/Platform/Push/PushTypeTests.cs new file mode 100644 index 0000000000..0d1e389410 --- /dev/null +++ b/test/Core.Test/Platform/Push/PushTypeTests.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; +using System.Reflection; +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Xunit; + +namespace Bit.Core.Test.Platform.Push; + +public class PushTypeTests +{ + [Fact] + public void AllEnumMembersHaveUniqueValue() + { + // No enum member should use the same value as another named member. + + var usedNumbers = new HashSet(); + var enumMembers = Enum.GetValues(); + + foreach (var enumMember in enumMembers) + { + if (!usedNumbers.Add((byte)enumMember)) + { + Assert.Fail($"Enum number value ({(byte)enumMember}) on {enumMember} is already in use."); + } + } + } + + [Fact] + public void AllEnumMembersHaveNotificationInfoAttribute() + { + // Every enum member should be annotated with [NotificationInfo] + + foreach (var member in typeof(PushType).GetMembers(BindingFlags.Public | BindingFlags.Static)) + { + var notificationInfoAttribute = member.GetCustomAttribute(); + if (notificationInfoAttribute is null) + { + Assert.Fail($"PushType.{member.Name} is missing a required [NotificationInfo(\"team-name\", typeof(MyType))] attribute."); + } + } + } + + [Fact] + public void AllEnumValuesAreInSequence() + { + // There should not be any gaps in the numbers defined for an enum, that being if someone last defined 22 + // the next number used should be 23 not 24 or any other number. + + var sortedValues = Enum.GetValues() + .Order() + .ToArray(); + + Debug.Assert(sortedValues.Length > 0); + + var lastValue = sortedValues[0]; + + foreach (var value in sortedValues[1..]) + { + var expectedValue = ++lastValue; + + Assert.Equal(expectedValue, value); + } + } +} diff --git a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs deleted file mode 100644 index a1bc2c6547..0000000000 --- a/test/Core.Test/Platform/Push/Services/MultiServicePushNotificationServiceTests.cs +++ /dev/null @@ -1,8 +0,0 @@ -#nullable enable - -namespace Bit.Core.Test.Platform.Push.Services; - -public class MultiServicePushNotificationServiceTests -{ - // TODO: Can add a couple tests here -} diff --git a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/Platform/PushRegistration/NotificationHubPushRegistrationServiceTests.cs similarity index 99% rename from test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs rename to test/Core.Test/Platform/PushRegistration/NotificationHubPushRegistrationServiceTests.cs index b30cd3dda8..43ac916ed6 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/Platform/PushRegistration/NotificationHubPushRegistrationServiceTests.cs @@ -1,6 +1,8 @@ #nullable enable using Bit.Core.Enums; -using Bit.Core.NotificationHub; +using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration; +using Bit.Core.Platform.PushRegistration.Internal; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Platform/PushRegistration/PushRegistrationServiceCollectionExtensionsTests.cs b/test/Core.Test/Platform/PushRegistration/PushRegistrationServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..19760f93ab --- /dev/null +++ b/test/Core.Test/Platform/PushRegistration/PushRegistrationServiceCollectionExtensionsTests.cs @@ -0,0 +1,108 @@ +using Bit.Core.Platform.Push; +using Bit.Core.Platform.PushRegistration.Internal; +using Bit.Core.Repositories; +using Bit.Core.Repositories.Noop; +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Xunit; + +namespace Bit.Core.Test.Platform.PushRegistration; + +public class PushRegistrationServiceCollectionExtensionsTests +{ + [Fact] + public void AddPushRegistration_Cloud_CreatesNotificationHubRegistrationService() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "false" }, + }); + + var pushRegistrationService = services.GetRequiredService(); + Assert.IsType(pushRegistrationService); + } + + [Fact] + public void AddPushRegistration_SelfHosted_NoOtherConfig_ReturnsNoopRegistrationService() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + }); + + var pushRegistrationService = services.GetRequiredService(); + Assert.IsType(pushRegistrationService); + } + + [Fact] + public void AddPushRegistration_SelfHosted_RelayConfig_ReturnsRelayRegistrationService() + { + var services = Build(new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, + { "GlobalSettings:Installation:Key", "some_key" }, + }); + + var pushRegistrationService = services.GetRequiredService(); + Assert.IsType(pushRegistrationService); + } + + [Fact] + public void AddPushRegistration_MultipleTimes_NoAdditionalServices() + { + var services = new ServiceCollection(); + + var config = new Dictionary + { + { "GlobalSettings:SelfHosted", "true" }, + { "GlobalSettings:PushRelayBaseUri", "https://example.com" }, + { "GlobalSettings:Installation:Key", "some_key" }, + }; + + AddServices(services, config); + + // Add services again + services.AddPushRegistration(); + + var provider = services.BuildServiceProvider(); + + Assert.Single(provider.GetServices()); + } + + private static ServiceProvider Build(Dictionary initialData) + { + var services = new ServiceCollection(); + + AddServices(services, initialData); + + return services.BuildServiceProvider(); + } + + private static void AddServices(IServiceCollection services, Dictionary initialData) + { + // A minimal service collection is always expected to have logging, config, and global settings + // pre-registered. + + services.AddLogging(); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(initialData) + .Build(); + + services.TryAddSingleton(config); + var globalSettings = new GlobalSettings(); + config.GetSection("GlobalSettings").Bind(globalSettings); + + services.TryAddSingleton(globalSettings); + services.TryAddSingleton(globalSettings); + + + // Temporary until AddPushRegistration can add it themselves directly. + services.TryAddSingleton(); + + services.AddPushRegistration(); + } +} diff --git a/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs b/test/Core.Test/Platform/PushRegistration/RelayPushRegistrationServiceTests.cs similarity index 95% rename from test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs rename to test/Core.Test/Platform/PushRegistration/RelayPushRegistrationServiceTests.cs index 062b4a96a8..1abadacd24 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushRegistrationServiceTests.cs +++ b/test/Core.Test/Platform/PushRegistration/RelayPushRegistrationServiceTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration.Internal; using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index b454a0c04b..f34a906404 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -4,8 +4,8 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.NotificationHub; using Bit.Core.Platform.Push; +using Bit.Core.Platform.PushRegistration; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 1d39d63ef7..92e80b073d 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; -using Bit.Core.Platform.Push.Internal; +using Bit.Core.Platform.PushRegistration.Internal; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Infrastructure.EntityFramework.Repositories; From 0074860cad0a4f42a12ca79ec2c49bfb947cf138 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:42:46 -0500 Subject: [PATCH 171/326] chore: remove account deprovisioning feature flag definition, refs PM-14614 (#6250) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2fbf7caffd..8944eec03b 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -106,7 +106,6 @@ public static class AuthenticationSchemes public static class FeatureFlagKeys { /* Admin Console Team */ - public const string AccountDeprovisioning = "pm-10308-account-deprovisioning"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string PolicyRequirements = "pm-14439-policy-requirements"; From 8ceb6f56217de10095172d899719edbdee179da7 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Wed, 27 Aug 2025 11:01:22 -0400 Subject: [PATCH 172/326] [PM-24278] Create Remove Individual Vault validator (#6139) --- .../ConfirmOrganizationUserCommand.cs | 2 +- ...anizationDataOwnershipPolicyRequirement.cs | 40 ++- .../PolicyServiceCollectionExtensions.cs | 2 + ...rganizationDataOwnershipPolicyValidator.cs | 68 +++++ .../OrganizationPolicyValidator.cs | 49 ++++ .../Repositories/ICollectionRepository.cs | 2 +- .../Repositories/CollectionRepository.cs | 2 +- .../Repositories/CollectionRepository.cs | 2 +- .../OrganizationPolicyDetailsCustomization.cs | 35 +++ .../AutoFixture/PolicyDetailsFixtures.cs | 2 + .../ConfirmOrganizationUserCommandTests.cs | 6 +- ...aOwnershipPolicyRequirementFactoryTests.cs | 86 ++++++- ...zationDataOwnershipPolicyValidatorTests.cs | 239 ++++++++++++++++++ .../OrganizationPolicyValidatorTests.cs | 188 ++++++++++++++ .../ImportCiphersAsyncCommandTests.cs | 3 +- .../Vault/Services/CipherServiceTests.cs | 3 +- ...ts.cs => UpsertDefaultCollectionsTests.cs} | 22 +- 17 files changed, 709 insertions(+), 42 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs create mode 100644 test/Core.Test/AdminConsole/AutoFixture/OrganizationPolicyDetailsCustomization.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs rename test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/{CreateDefaultCollectionsTests.cs => UpsertDefaultCollectionsTests.cs} (91%) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index cbedb6355d..83ec244c47 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -278,6 +278,6 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand return; } - await _collectionRepository.CreateDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName); + await _collectionRepository.UpsertDefaultCollectionsAsync(organizationId, eligibleOrganizationUserIds, defaultUserCollectionName); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs index 7ccb3f7807..cb72a51850 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; @@ -24,19 +25,19 @@ public enum OrganizationDataOwnershipState /// public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement { - private readonly IEnumerable _organizationIdsWithPolicyEnabled; + private readonly IEnumerable _policyDetails; /// /// The organization data ownership state for the user. /// - /// - /// The collection of Organization IDs that have the Organization Data Ownership policy enabled. + /// + /// An enumerable collection of PolicyDetails for the organizations. /// public OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState organizationDataOwnershipState, - IEnumerable organizationIdsWithPolicyEnabled) + IEnumerable policyDetails) { - _organizationIdsWithPolicyEnabled = organizationIdsWithPolicyEnabled ?? []; + _policyDetails = policyDetails; State = organizationDataOwnershipState; } @@ -46,14 +47,34 @@ public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement public OrganizationDataOwnershipState State { get; } /// - /// Returns true if the Organization Data Ownership policy is enforced in that organization. + /// Gets a default collection request for enforcing the Organization Data Ownership policy. + /// Only confirmed users are applicable. + /// This indicates whether the user should have a default collection created for them when the policy is enabled, + /// and if so, the relevant OrganizationUserId to create the collection for. /// - public bool RequiresDefaultCollection(Guid organizationId) + /// The organization ID to create the request for. + /// A DefaultCollectionRequest containing the OrganizationUserId and a flag indicating whether to create a default collection. + public DefaultCollectionRequest GetDefaultCollectionRequestOnPolicyEnable(Guid organizationId) { - return _organizationIdsWithPolicyEnabled.Contains(organizationId); + var policyDetail = _policyDetails + .FirstOrDefault(p => p.OrganizationId == organizationId); + + if (policyDetail != null && policyDetail.HasStatus([OrganizationUserStatusType.Confirmed])) + { + return new DefaultCollectionRequest(policyDetail.OrganizationUserId, true); + } + + var noCollectionNeeded = new DefaultCollectionRequest(Guid.Empty, false); + return noCollectionNeeded; } } +public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection) +{ + public readonly bool ShouldCreateDefaultCollection = ShouldCreateDefaultCollection; + public readonly Guid OrganizationUserId = OrganizationUserId; +} + public class OrganizationDataOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory { public override PolicyType PolicyType => PolicyType.OrganizationDataOwnership; @@ -63,10 +84,9 @@ public class OrganizationDataOwnershipPolicyRequirementFactory : BasePolicyRequi var organizationDataOwnershipState = policyDetails.Any() ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled; - var organizationIdsWithPolicyEnabled = policyDetails.Select(p => p.OrganizationId).ToHashSet(); return new OrganizationDataOwnershipPolicyRequirement( organizationDataOwnershipState, - organizationIdsWithPolicyEnabled); + policyDetails); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index e31e9d44c9..12dd3f973d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -27,6 +27,8 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + // This validator will be hooked up in https://bitwarden.atlassian.net/browse/PM-24279. + // services.AddScoped(); } private static void AddPolicyRequirements(this IServiceCollection services) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs new file mode 100644 index 0000000000..2471bda647 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs @@ -0,0 +1,68 @@ +#nullable enable + +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.Repositories; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public class OrganizationDataOwnershipPolicyValidator( + IPolicyRepository policyRepository, + ICollectionRepository collectionRepository, + IEnumerable> factories, + IFeatureService featureService, + ILogger logger) + : OrganizationPolicyValidator(policyRepository, factories) +{ + public override PolicyType Type => PolicyType.OrganizationDataOwnership; + + public override IEnumerable RequiredPolicies => []; + + public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); + + public override async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) + { + return; + } + + if (currentPolicy?.Enabled != true && policyUpdate.Enabled) + { + await UpsertDefaultCollectionsForUsersAsync(policyUpdate); + } + } + + private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate) + { + var requirements = await GetUserPolicyRequirementsByOrganizationIdAsync(policyUpdate.OrganizationId, policyUpdate.Type); + + var userOrgIds = requirements + .Select(requirement => requirement.GetDefaultCollectionRequestOnPolicyEnable(policyUpdate.OrganizationId)) + .Where(request => request.ShouldCreateDefaultCollection) + .Select(request => request.OrganizationUserId); + + if (!userOrgIds.Any()) + { + logger.LogError("No UserOrganizationIds found for {OrganizationId}", policyUpdate.OrganizationId); + return; + } + + await collectionRepository.UpsertDefaultCollectionsAsync( + policyUpdate.OrganizationId, + userOrgIds, + GetDefaultUserCollectionName()); + } + + private static string GetDefaultUserCollectionName() + { + // TODO: https://bitwarden.atlassian.net/browse/PM-24279 + const string temporaryPlaceHolderValue = "Default"; + return temporaryPlaceHolderValue; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs new file mode 100644 index 0000000000..33667b829c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs @@ -0,0 +1,49 @@ +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.Repositories; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +public abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable> factories) : IPolicyValidator +{ + public abstract PolicyType Type { get; } + + public abstract IEnumerable RequiredPolicies { get; } + + protected async Task> GetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement + { + var factory = factories.OfType>().SingleOrDefault(); + if (factory is null) + { + throw new NotImplementedException("No Requirement Factory found for " + typeof(T)); + } + + var policyDetails = await policyRepository.GetPolicyDetailsByOrganizationIdAsync(organizationId, policyType); + var policyDetailGroups = policyDetails.GroupBy(policyDetail => policyDetail.UserId); + var requirements = new List(); + + foreach (var policyDetailGroup in policyDetailGroups) + { + var filteredPolicies = policyDetailGroup + .Where(factory.Enforce) + // Prevent deferred execution from causing inconsistent tests. + .ToList(); + + requirements.Add(factory.Create(filteredPolicies)); + } + + return requirements; + } + + public abstract Task OnSaveSideEffectsAsync( + PolicyUpdate policyUpdate, + Policy? currentPolicy + ); + + public abstract Task ValidateAsync( + PolicyUpdate policyUpdate, + Policy? currentPolicy + ); +} diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 70bda3eb13..ca3e52751c 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -63,5 +63,5 @@ public interface ICollectionRepository : IRepository Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable collectionIds, IEnumerable users, IEnumerable groups); - Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName); + Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName); } diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index 77fbdff3ae..ad00ac7086 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -326,7 +326,7 @@ public class CollectionRepository : Repository, ICollectionRep } } - public async Task CreateDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName) + public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName) { if (!affectedOrgUserIds.Any()) { diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 569e541163..021b5bcf16 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -793,7 +793,7 @@ public class CollectionRepository : Repository affectedOrgUserIds, string defaultCollectionName) + public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName) { if (!affectedOrgUserIds.Any()) { diff --git a/test/Core.Test/AdminConsole/AutoFixture/OrganizationPolicyDetailsCustomization.cs b/test/Core.Test/AdminConsole/AutoFixture/OrganizationPolicyDetailsCustomization.cs new file mode 100644 index 0000000000..cf16563b8c --- /dev/null +++ b/test/Core.Test/AdminConsole/AutoFixture/OrganizationPolicyDetailsCustomization.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.Test.AdminConsole.AutoFixture; + +internal class OrganizationPolicyDetailsCustomization( + PolicyType policyType, + OrganizationUserType userType, + bool isProvider, + OrganizationUserStatusType userStatus) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.PolicyType, policyType) + .With(o => o.OrganizationUserType, userType) + .With(o => o.IsProvider, isProvider) + .With(o => o.OrganizationUserStatus, userStatus) + .Without(o => o.PolicyData)); // avoid autogenerating invalid json data + } +} + +public class OrganizationPolicyDetailsAttribute( + PolicyType policyType, + OrganizationUserType userType = OrganizationUserType.User, + bool isProvider = false, + OrganizationUserStatusType userStatus = OrganizationUserStatusType.Confirmed) : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) + => new OrganizationPolicyDetailsCustomization(policyType, userType, isProvider, userStatus); +} diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs index 87ea390cb6..39d7732198 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyDetailsFixtures.cs @@ -33,3 +33,5 @@ public class PolicyDetailsAttribute( public override ICustomization GetCustomization(ParameterInfo parameter) => new PolicyDetailsCustomization(policyType, userType, isProvider, userStatus); } + + diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index a8219ebcaa..31938fe4fc 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -479,7 +479,7 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency() .Received(1) - .CreateDefaultCollectionsAsync( + .UpsertDefaultCollectionsAsync( organization.Id, Arg.Is>(ids => ids.Contains(orgUser.Id)), collectionName); @@ -505,7 +505,7 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency() .DidNotReceive() - .CreateDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] @@ -531,6 +531,6 @@ public class ConfirmOrganizationUserCommandTests await sutProvider.GetDependency() .DidNotReceive() - .CreateDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs index 95037efb97..ab4788c808 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirementFactoryTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -30,24 +31,85 @@ public class OrganizationDataOwnershipPolicyRequirementFactoryTests } [Theory, BitAutoData] - public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse( - Guid organizationId, - SutProvider sutProvider) + public void PolicyType_ReturnsOrganizationDataOwnership(SutProvider sutProvider) { - var actual = sutProvider.Sut.Create([]); - - Assert.False(actual.RequiresDefaultCollection(organizationId)); + Assert.Equal(PolicyType.OrganizationDataOwnership, sutProvider.Sut.PolicyType); } [Theory, BitAutoData] - public void RequiresDefaultCollection_WithOrganizationDataOwnershipPolicies_ReturnsCorrectResult( - [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies, - Guid nonPolicyOrganizationId, + public void GetDefaultCollectionRequestOnPolicyEnable_WithConfirmedUser_ReturnsTrue( + [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Confirmed)] PolicyDetails[] policies, + SutProvider sutProvider) + { + // Arrange + var requirement = sutProvider.Sut.Create(policies); + var expectedOrganizationUserId = policies[0].OrganizationUserId; + var organizationId = policies[0].OrganizationId; + + // Act + var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId); + + // Assert + Assert.Equal(expectedOrganizationUserId, result.OrganizationUserId); + Assert.True(result.ShouldCreateDefaultCollection); + } + + [Theory, BitAutoData] + public void GetDefaultCollectionRequestOnPolicyEnable_WithAcceptedUser_ReturnsFalse( + [PolicyDetails(PolicyType.OrganizationDataOwnership, userStatus: OrganizationUserStatusType.Accepted)] PolicyDetails[] policies, SutProvider sutProvider) { - var actual = sutProvider.Sut.Create(policies); + // Arrange + var requirement = sutProvider.Sut.Create(policies); + var organizationId = policies[0].OrganizationId; - Assert.True(actual.RequiresDefaultCollection(policies[0].OrganizationId)); - Assert.False(actual.RequiresDefaultCollection(nonPolicyOrganizationId)); + // Act + var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId); + + // Assert + Assert.Equal(Guid.Empty, result.OrganizationUserId); + Assert.False(result.ShouldCreateDefaultCollection); + } + + [Theory, BitAutoData] + public void GetDefaultCollectionRequestOnPolicyEnable_WithNoPolicies_ReturnsFalse( + SutProvider sutProvider) + { + // Arrange + var requirement = sutProvider.Sut.Create([]); + var organizationId = Guid.NewGuid(); + + // Act + var result = requirement.GetDefaultCollectionRequestOnPolicyEnable(organizationId); + + // Assert + Assert.Equal(Guid.Empty, result.OrganizationUserId); + Assert.False(result.ShouldCreateDefaultCollection); + } + + [Theory, BitAutoData] + public void GetDefaultCollectionRequestOnPolicyEnable_WithMixedStatuses( + [PolicyDetails(PolicyType.OrganizationDataOwnership)] PolicyDetails[] policies, + SutProvider sutProvider) + { + // Arrange + var requirement = sutProvider.Sut.Create(policies); + + var confirmedPolicy = policies[0]; + var acceptedPolicy = policies[1]; + + confirmedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Confirmed; + acceptedPolicy.OrganizationUserStatus = OrganizationUserStatusType.Accepted; + + // Act + var confirmedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(confirmedPolicy.OrganizationId); + var acceptedResult = requirement.GetDefaultCollectionRequestOnPolicyEnable(acceptedPolicy.OrganizationId); + + // Assert + Assert.Equal(Guid.Empty, acceptedResult.OrganizationUserId); + Assert.False(acceptedResult.ShouldCreateDefaultCollection); + + Assert.Equal(confirmedPolicy.OrganizationUserId, confirmedResult.OrganizationUserId); + Assert.True(confirmedResult.ShouldCreateDefaultCollection); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs new file mode 100644 index 0000000000..2569bc6988 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs @@ -0,0 +1,239 @@ +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.PolicyRequirements; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +[SutProviderCustomize] +public class OrganizationDataOwnershipPolicyValidatorTests +{ + private const string _defaultUserCollectionName = "Default"; + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_FeatureFlagDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(false); + + // Act + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + SutProvider sutProvider) + { + // Arrange + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + policyUpdate.Enabled = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + // Act + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_PolicyBeingDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + SutProvider sutProvider) + { + // Arrange + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + // Act + await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task OnSaveSideEffectsAsync_WhenNoUsersExist_ShouldLogError( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + OrganizationDataOwnershipPolicyRequirementFactory factory) + { + // Arrange + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + policyUpdate.Enabled = true; + + var policyRepository = ArrangePolicyRepositoryWithOutUsers(); + var collectionRepository = Substitute.For(); + var logger = Substitute.For>(); + + var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + + // Act + await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + + // Assert + await collectionRepository + .DidNotReceive() + .UpsertDefaultCollectionsAsync( + Arg.Any(), + Arg.Any>(), + Arg.Any()); + + const string expectedErrorMessage = "No UserOrganizationIds found for"; + + logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => (o.ToString() ?? "").Contains(expectedErrorMessage)), + Arg.Any(), + Arg.Any>()); + } + + public static IEnumerable ShouldUpsertDefaultCollectionsTestCases() + { + yield return WithExistingPolicy(); + + yield return WithNoExistingPolicy(); + yield break; + + object?[] WithExistingPolicy() + { + var organizationId = Guid.NewGuid(); + var policyUpdate = new PolicyUpdate + { + OrganizationId = organizationId, + Type = PolicyType.OrganizationDataOwnership, + Enabled = true + }; + var currentPolicy = new Policy + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + Type = PolicyType.OrganizationDataOwnership, + Enabled = false + }; + + return new object?[] + { + policyUpdate, + currentPolicy + }; + } + + object?[] WithNoExistingPolicy() + { + var policyUpdate = new PolicyUpdate + { + OrganizationId = new Guid(), + Type = PolicyType.OrganizationDataOwnership, + Enabled = true + }; + + const Policy currentPolicy = null; + + return new object?[] + { + policyUpdate, + currentPolicy + }; + } + } + [Theory, BitAutoData] + [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))] + public async Task OnSaveSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy? currentPolicy, + [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable orgPolicyDetails, + OrganizationDataOwnershipPolicyRequirementFactory factory) + { + // Arrange + foreach (var policyDetail in orgPolicyDetails) + { + policyDetail.OrganizationId = policyUpdate.OrganizationId; + } + + var policyRepository = ArrangePolicyRepository(orgPolicyDetails); + var collectionRepository = Substitute.For(); + var logger = Substitute.For>(); + + var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + + // Act + await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + + // Assert + await collectionRepository + .Received(1) + .UpsertDefaultCollectionsAsync( + policyUpdate.OrganizationId, + Arg.Is>(ids => ids.Count() == 3), + _defaultUserCollectionName); + } + + private static IPolicyRepository ArrangePolicyRepositoryWithOutUsers() + { + return ArrangePolicyRepository([]); + } + + private static IPolicyRepository ArrangePolicyRepository(IEnumerable policyDetails) + { + var policyRepository = Substitute.For(); + + policyRepository + .GetPolicyDetailsByOrganizationIdAsync(Arg.Any(), PolicyType.OrganizationDataOwnership) + .Returns(policyDetails); + return policyRepository; + } + + private static OrganizationDataOwnershipPolicyValidator ArrangeSut( + OrganizationDataOwnershipPolicyRequirementFactory factory, + IPolicyRepository policyRepository, + ICollectionRepository collectionRepository, + ILogger logger = null!) + { + logger ??= Substitute.For>(); + + var featureService = Substitute.For(); + featureService + .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + .Returns(true); + + var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService, logger); + return sut; + } + +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs new file mode 100644 index 0000000000..aec1230423 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs @@ -0,0 +1,188 @@ +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.PolicyRequirements; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; + +[SutProviderCustomize] +public class OrganizationPolicyValidatorTests +{ + [Theory, BitAutoData] + public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithNoFactory_ThrowsNotImplementedException( + Guid organizationId, + SutProvider sutProvider) + { + // Arrange + var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency(), []); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sut.TestGetUserPolicyRequirementsByOrganizationIdAsync( + organizationId, PolicyType.TwoFactorAuthentication)); + + Assert.Contains("No Requirement Factory found for", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithMultipleUsers_GroupsByUserId( + Guid organizationId, + Guid userId1, + Guid userId2, + SutProvider sutProvider) + { + // Arrange + var policyDetails = new List + { + new() { UserId = userId1, OrganizationId = organizationId }, + new() { UserId = userId1, OrganizationId = Guid.NewGuid() }, + new() { UserId = userId2, OrganizationId = organizationId } + }; + + var factory = Substitute.For>(); + factory.Create(Arg.Any>()).Returns(new TestPolicyRequirement()); + factory.Enforce(Arg.Any()).Returns(true); + + sutProvider.GetDependency() + .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication) + .Returns(policyDetails); + + var factories = new List> { factory }; + var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency(), factories); + + // Act + var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync( + organizationId, PolicyType.TwoFactorAuthentication); + + // Assert + Assert.Equal(2, result.Count()); + + factory.Received(2).Create(Arg.Any>()); + factory.Received(1).Create(Arg.Is>( + results => results.Count() == 1 && results.First().UserId == userId2)); + factory.Received(1).Create(Arg.Is>( + results => results.Count() == 2 && results.First().UserId == userId1)); + } + + [Theory, BitAutoData] + public async Task GetUserPolicyRequirementsByOrganizationIdAsync_ShouldEnforceFilters( + Guid organizationId, + Guid userId, + SutProvider sutProvider) + { + // Arrange + var adminUser = new OrganizationPolicyDetails() + { + UserId = userId, + OrganizationId = organizationId, + OrganizationUserType = OrganizationUserType.Admin + }; + + var user = new OrganizationPolicyDetails() + { + UserId = userId, + OrganizationId = organizationId, + OrganizationUserType = OrganizationUserType.User + }; + + var policyDetails = new List + { + adminUser, + user + }; + sutProvider.GetDependency() + .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication) + .Returns(policyDetails); + + var factory = Substitute.For>(); + factory.Create(Arg.Any>()).Returns(new TestPolicyRequirement()); + factory.Enforce(Arg.Is(p => p.OrganizationUserType == OrganizationUserType.Admin)) + .Returns(true); + factory.Enforce(Arg.Is(p => p.OrganizationUserType == OrganizationUserType.User)) + .Returns(false); + + var factories = new List> { factory }; + var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency(), factories); + + // Act + var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync( + organizationId, PolicyType.TwoFactorAuthentication); + + // Assert + Assert.Single(result); + + factory.Received(1).Create(Arg.Is>(policies => + policies.Count() == 1 && policies.First().OrganizationUserType == OrganizationUserType.Admin)); + + factory.Received(1).Enforce(Arg.Is(p => ReferenceEquals(p, adminUser))); + factory.Received(1).Enforce(Arg.Is(p => ReferenceEquals(p, user))); + factory.Received(2).Enforce(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetUserPolicyRequirementsByOrganizationIdAsync_WithEmptyPolicyDetails_ReturnsEmptyCollection( + Guid organizationId, + SutProvider sutProvider) + { + // Arrange + var factory = Substitute.For>(); + + sutProvider.GetDependency() + .GetPolicyDetailsByOrganizationIdAsync(organizationId, PolicyType.TwoFactorAuthentication) + .Returns(new List()); + + var factories = new List> { factory }; + var sut = new TestOrganizationPolicyValidator(sutProvider.GetDependency(), factories); + + // Act + var result = await sut.TestGetUserPolicyRequirementsByOrganizationIdAsync( + organizationId, PolicyType.TwoFactorAuthentication); + + // Assert + Assert.Empty(result); + factory.DidNotReceive().Create(Arg.Any>()); + } +} + +public class TestOrganizationPolicyValidator : OrganizationPolicyValidator +{ + public TestOrganizationPolicyValidator( + IPolicyRepository policyRepository, + IEnumerable>? factories = null) + : base(policyRepository, factories ?? []) + { + } + + public override PolicyType Type => PolicyType.TwoFactorAuthentication; + + public override IEnumerable RequiredPolicies => []; + + public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + return Task.FromResult(""); + } + + public override Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + { + return Task.CompletedTask; + } + + public async Task> TestGetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) + where T : IPolicyRequirement + { + return await GetUserPolicyRequirementsByOrganizationIdAsync(organizationId, policyType); + } + +} + +public class TestPolicyRequirement : IPolicyRequirement +{ +} diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 1b50779c57..0cb0deaf52 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; @@ -120,7 +121,7 @@ public class ImportCiphersAsyncCommandTests .GetAsync(userId) .Returns(new OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState.Enabled, - [Guid.NewGuid()])); + [new PolicyDetails()])); var folderRelationships = new List>(); diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 0cee6530c2..55db5a9143 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; @@ -173,7 +174,7 @@ public class CipherServiceTests .GetAsync(savingUserId) .Returns(new OrganizationDataOwnershipPolicyRequirement( OrganizationDataOwnershipState.Enabled, - [Guid.NewGuid()])); + [new PolicyDetails()])); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null)); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionsTests.cs similarity index 91% rename from test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs rename to test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionsTests.cs index d85cc1e813..64dffa473f 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CreateDefaultCollectionsTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/UpsertDefaultCollectionsTests.cs @@ -6,10 +6,10 @@ using Xunit; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; -public class CreateDefaultCollectionsTests +public class UpsertDefaultCollectionsTests { - [DatabaseTheory, DatabaseData] - public async Task CreateDefaultCollectionsAsync_ShouldCreateDefaultCollection_WhenUsersDoNotHaveDefaultCollection( + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionsAsync_ShouldCreateDefaultCollection_WhenUsersDoNotHaveDefaultCollection( IOrganizationRepository organizationRepository, IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -28,7 +28,7 @@ public class CreateDefaultCollectionsTests var defaultCollectionName = $"default-name-{organization.Id}"; // Act - await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); // Assert await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); @@ -36,8 +36,8 @@ public class CreateDefaultCollectionsTests await CleanupAsync(organizationRepository, userRepository, organization, resultOrganizationUsers); } - [DatabaseTheory, DatabaseData] - public async Task CreateDefaultCollectionsAsync_ShouldUpsertCreateDefaultCollection_ForUsersWithAndWithoutDefaultCollectionsExist( + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionsAsync_ShouldUpsertCreateDefaultCollection_ForUsersWithAndWithoutDefaultCollectionsExist( IOrganizationRepository organizationRepository, IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -66,7 +66,7 @@ public class CreateDefaultCollectionsTests var affectedOrgUserIds = affectedOrgUsers.Select(organizationUser => organizationUser.Id); // Act - await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); // Assert await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, arrangedOrganizationUsers, organization.Id); @@ -74,8 +74,8 @@ public class CreateDefaultCollectionsTests await CleanupAsync(organizationRepository, userRepository, organization, affectedOrgUsers); } - [DatabaseTheory, DatabaseData] - public async Task CreateDefaultCollectionsAsync_ShouldNotCreateDefaultCollection_WhenUsersAlreadyHaveOne( + [Theory, DatabaseData] + public async Task UpsertDefaultCollectionsAsync_ShouldNotCreateDefaultCollection_WhenUsersAlreadyHaveOne( IOrganizationRepository organizationRepository, IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -96,7 +96,7 @@ public class CreateDefaultCollectionsTests await CreateUsersWithExistingDefaultCollectionsAsync(collectionRepository, organization.Id, affectedOrgUserIds, defaultCollectionName, resultOrganizationUsers); // Act - await collectionRepository.CreateDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); + await collectionRepository.UpsertDefaultCollectionsAsync(organization.Id, affectedOrgUserIds, defaultCollectionName); // Assert await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organization.Id); @@ -108,7 +108,7 @@ public class CreateDefaultCollectionsTests Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName, OrganizationUser[] resultOrganizationUsers) { - await collectionRepository.CreateDefaultCollectionsAsync(organizationId, affectedOrgUserIds, defaultCollectionName); + await collectionRepository.UpsertDefaultCollectionsAsync(organizationId, affectedOrgUserIds, defaultCollectionName); await AssertAllUsersHaveOneDefaultCollectionAsync(collectionRepository, resultOrganizationUsers, organizationId); } From d24cbf25c7f9bac159528e93c6160bfc3ecc91a2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:12:23 +0000 Subject: [PATCH 173/326] [deps] Tools: Update aws-sdk-net monorepo (#6254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 0dbb8e3023..25e74d8aee 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 5dfed7623b626b5c3da9fd90d0668f21f3f5b474 Mon Sep 17 00:00:00 2001 From: Maksym Sorokin Date: Thu, 28 Aug 2025 15:36:02 +0200 Subject: [PATCH 174/326] Fixed Nginx entrypoint to cp with preserve owner (#6249) If user cleanly follow install instructions Setup app will create nginx `default.conf` (and other files) with `644` permission owned by `bitwarden:bitwarden`. During Nginx entrypoint script it copies generated `default.conf` to `/etc/nginx/conf.d/` but without `-p` flag new file permissions would be `root:root 644`. Then during startup Nginx will start as `bitwarden` user, which will not cause any issues by itself as `default.conf` is still readable by the world. The issue is that for some reason some users have their Nginx config file (or sometimes even entire `bwdata` recursively) have `600` or `700` permissions. In this case Nginx will fail to start due to `default.conf` not readable by `bitwarden` user. I assume that root cause is that some users mistakenly run `sudo chmod -R 700 /opt/bitwarden` from Linux installation guide after they have run `./bitwarden.sh install`. Or maybe some older version of Setup app where creating `default.conf` with `600` permissions and users are using very legacy installations. Whatever may be the case I do not see any harm with copying with `-p` it even looks to me that this was the intended behavior. This will both fix the issue for mentioned users and preserve permission structure aligned with other files. --- util/Nginx/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/Nginx/entrypoint.sh b/util/Nginx/entrypoint.sh index 0d4fa73802..627430ee79 100644 --- a/util/Nginx/entrypoint.sh +++ b/util/Nginx/entrypoint.sh @@ -30,7 +30,7 @@ mkhomedir_helper $USERNAME # The rest... chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/nginx/*.conf /etc/nginx/conf.d/ +cp -p /etc/bitwarden/nginx/*.conf /etc/nginx/conf.d/ mkdir -p /etc/letsencrypt chown -R $USERNAME:$GROUPNAME /etc/letsencrypt mkdir -p /etc/ssl From 5a96f6dccec0a0638a17369712e59124ccc141af Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:14:00 -0400 Subject: [PATCH 175/326] chore(feature-flags): Remove storage-reseed feature flag --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8944eec03b..dec605f4bc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -188,7 +188,6 @@ public static class FeatureFlagKeys /* Platform Team */ public const string PersistPopupView = "persist-popup-view"; - public const string StorageReseedRefactor = "storage-reseed-refactor"; public const string WebPush = "web-push"; public const string IpcChannelFramework = "ipc-channel-framework"; public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; From 1c60b805bf80c190332f954e0922d7544eb77284 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:45:32 -0400 Subject: [PATCH 176/326] chore(feature-flag): [PM-19665] Remove web-push feature flag * Remove storage-reseed feature flag * Remove web-push feature flag. * Removed check for web push enabled. * Linting --- src/Api/Models/Response/ConfigResponseModel.cs | 8 +++----- src/Core/Constants.cs | 1 - .../Factories/WebApplicationFactoryBase.cs | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Api/Models/Response/ConfigResponseModel.cs b/src/Api/Models/Response/ConfigResponseModel.cs index d748254206..20bc3f9e10 100644 --- a/src/Api/Models/Response/ConfigResponseModel.cs +++ b/src/Api/Models/Response/ConfigResponseModel.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Core; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Services; @@ -46,8 +45,7 @@ public class ConfigResponseModel : ResponseModel Sso = globalSettings.BaseServiceUri.Sso }; FeatureStates = featureService.GetAll(); - var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false; - Push = PushSettings.Build(webPushEnabled, globalSettings); + Push = PushSettings.Build(globalSettings); Settings = new ServerSettingsResponseModel { DisableUserRegistration = globalSettings.DisableUserRegistration @@ -76,9 +74,9 @@ public class PushSettings public PushTechnologyType PushTechnology { get; private init; } public string VapidPublicKey { get; private init; } - public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings) + public static PushSettings Build(IGlobalSettings globalSettings) { - var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null; + var vapidPublicKey = globalSettings.WebPush.VapidPublicKey; var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR; return new() { diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index dec605f4bc..56c6cd5476 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -188,7 +188,6 @@ public static class FeatureFlagKeys /* Platform Team */ public const string PersistPopupView = "persist-popup-view"; - public const string WebPush = "web-push"; public const string IpcChannelFramework = "ipc-channel-framework"; public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index 92e80b073d..d05f940c09 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -153,7 +153,6 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory // Web push notifications { "globalSettings:webPush:vapidPublicKey", "BGBtAM0bU3b5jsB14IjBYarvJZ6rWHilASLudTTYDDBi7a-3kebo24Yus_xYeOMZ863flAXhFAbkL6GVSrxgErg" }, - { "globalSettings:launchDarkly:flagValues:web-push", "true" }, }; // Some database drivers modify the connection string From 9a6cdcd5e2b02bb1f5753dd9a1c2477b880c6e39 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:14:45 -0400 Subject: [PATCH 177/326] chore(feature-flag): [PM-18516] Remove pm-9112-device-approval-persistence flag * Remove persistence feature flags * Added back 2FA value. --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 56c6cd5476..f78cc28032 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -116,7 +116,6 @@ public static class FeatureFlagKeys public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; /* Auth Team */ - public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; From 697fa6fdbc220173319fdee091c2b2fbf8296d99 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:39:49 -0400 Subject: [PATCH 178/326] chore(feature-flag): [PM-25336] Remove unauth-ui-refresh flag --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f78cc28032..39bd3fea5d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -118,7 +118,6 @@ public static class FeatureFlagKeys /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; - public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; From 101e29b3541248982ad18f60c56bd790dfaca949 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 2 Sep 2025 10:52:23 -0400 Subject: [PATCH 179/326] [PM-15354] fix EF implementation to match dapper (missing null check) (#6261) * fix EF implementation to match dapper (missing null check) * cleanup --- .../Repositories/OrganizationDomainRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index e7bee0cdfd..0ddf80130e 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -152,7 +152,7 @@ public class OrganizationDomainRepository : Repository x.LastCheckedDate < DateTime.UtcNow.AddDays(-expirationPeriod)) + .Where(x => x.LastCheckedDate < DateTime.UtcNow.AddDays(-expirationPeriod) && x.VerifiedDate == null) .ToListAsync(); dbContext.OrganizationDomains.RemoveRange(expiredDomains); return await dbContext.SaveChangesAsync() > 0; From cb1db262cacc7a5c95e5ed50ba94e4b9c1ad3ae6 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:18:36 -0400 Subject: [PATCH 180/326] chore(feature-flag): [PM-18179] Remove pm-17128-recovery-code-login feature flag * Rmoved feature flag and obsolete endpoint * Removed obsolete method. --- .../Auth/Controllers/TwoFactorController.cs | 15 --------- src/Core/Constants.cs | 1 - src/Core/Services/IUserService.cs | 3 -- .../Services/Implementations/UserService.cs | 33 ------------------- 4 files changed, 52 deletions(-) diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 96b64f16fc..4155489daa 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -409,21 +409,6 @@ public class TwoFactorController : Controller return response; } - /// - /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175. - /// - [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")] - [HttpPost("recover")] - [AllowAnonymous] - public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model) - { - if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode)) - { - await Task.Delay(2000); - throw new BadRequestException(string.Empty, "Invalid information. Try again."); - } - } - [Obsolete("Leaving this for backwards compatibility on clients")] [HttpGet("get-device-verification-settings")] public Task GetDeviceVerificationSettings() diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 39bd3fea5d..352daee862 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -121,7 +121,6 @@ public static class FeatureFlagKeys public const string BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; - public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string Otp6Digits = "pm-18612-otp-6-digits"; public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email"; diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 8457a9c128..ef602be93a 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -90,9 +90,6 @@ public interface IUserService void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true); - [Obsolete("To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.")] - Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); - /// /// This method is used by the TwoFactorAuthenticationValidator to recover two /// factor for a user. This allows users to be logged in after a successful recovery diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 0da565c4ba..16e298d177 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -865,39 +865,6 @@ public class UserService : UserManager, IUserService } } - /// - /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175. - /// - [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")] - public async Task RecoverTwoFactorAsync(string email, string secret, string recoveryCode) - { - var user = await _userRepository.GetByEmailAsync(email); - if (user == null) - { - // No user exists. Do we want to send an email telling them this in the future? - return false; - } - - if (!await VerifySecretAsync(user, secret)) - { - return false; - } - - if (!CoreHelpers.FixedTimeEquals(user.TwoFactorRecoveryCode, recoveryCode)) - { - return false; - } - - user.TwoFactorProviders = null; - user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false); - await SaveUserAsync(user); - await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress); - await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa); - await CheckPoliciesOnTwoFactorRemovalAsync(user); - - return true; - } - public async Task RecoverTwoFactorAsync(User user, string recoveryCode) { if (!CoreHelpers.FixedTimeEquals( From a180317509b4b200185d2ffbbfdccf9f49560a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Tue, 2 Sep 2025 18:30:53 +0200 Subject: [PATCH 181/326] [PM-25182] Improve swagger OperationIDs: Part 1 (#6229) * Improve swagger OperationIDs: Part 1 * Fix tests and fmt * Improve docs and add more tests * Fmt * Improve Swagger OperationIDs for Auth * Fix review feedback * Use generic getcustomattributes * Format * replace swaggerexclude by split+obsolete * Format * Some remaining excludes --- dev/generate_openapi_files.ps1 | 9 ++ .../Auth/Controllers/AccountsController.cs | 32 ++++++- .../Controllers/AuthRequestsController.cs | 2 +- .../Controllers/EmergencyAccessController.cs | 18 +++- .../Auth/Controllers/TwoFactorController.cs | 67 +++++++++++++-- src/Api/Controllers/CollectionsController.cs | 26 +++++- src/Api/Controllers/DevicesController.cs | 55 ++++++++++-- src/Api/Controllers/InfoController.cs | 8 +- src/Api/Controllers/SettingsController.cs | 8 +- .../Utilities/ServiceCollectionExtensions.cs | 10 +-- src/Identity/Controllers/InfoController.cs | 8 +- src/Identity/Startup.cs | 10 +-- .../Swagger/ActionNameOperationFilter.cs | 25 ++++++ ...heckDuplicateOperationIdsDocumentFilter.cs | 80 +++++++++++++++++ src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs | 33 +++++++ .../AuthRequestsControllerTests.cs | 2 +- .../Controllers/DevicesControllerTests.cs | 4 +- .../Controllers/CollectionsControllerTests.cs | 4 +- .../ActionNameOperationFilterTest.cs | 67 +++++++++++++++ ...DuplicateOperationIdsDocumentFilterTest.cs | 84 ++++++++++++++++++ test/SharedWeb.Test/SharedWeb.Test.csproj | 1 + test/SharedWeb.Test/SwaggerDocUtil.cs | 85 +++++++++++++++++++ 22 files changed, 583 insertions(+), 55 deletions(-) create mode 100644 src/SharedWeb/Swagger/ActionNameOperationFilter.cs create mode 100644 src/SharedWeb/Swagger/CheckDuplicateOperationIdsDocumentFilter.cs create mode 100644 src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs create mode 100644 test/SharedWeb.Test/ActionNameOperationFilterTest.cs create mode 100644 test/SharedWeb.Test/CheckDuplicateOperationIdsDocumentFilterTest.cs create mode 100644 test/SharedWeb.Test/SwaggerDocUtil.cs diff --git a/dev/generate_openapi_files.ps1 b/dev/generate_openapi_files.ps1 index 02470a0b1d..9eca7dc734 100644 --- a/dev/generate_openapi_files.ps1 +++ b/dev/generate_openapi_files.ps1 @@ -11,9 +11,18 @@ dotnet tool restore Set-Location "./src/Identity" dotnet build dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1" +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} # Api internal & public Set-Location "../../src/Api" dotnet build dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal" +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public" +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index f197f1270b..0bed7c29c4 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -344,7 +344,6 @@ public class AccountsController : Controller } [HttpPut("profile")] - [HttpPost("profile")] public async Task PutProfile([FromBody] UpdateProfileRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -363,8 +362,14 @@ public class AccountsController : Controller return response; } + [HttpPost("profile")] + [Obsolete("This endpoint is deprecated. Use PUT /profile instead.")] + public async Task PostProfile([FromBody] UpdateProfileRequestModel model) + { + return await PutProfile(model); + } + [HttpPut("avatar")] - [HttpPost("avatar")] public async Task PutAvatar([FromBody] UpdateAvatarRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -382,6 +387,13 @@ public class AccountsController : Controller return response; } + [HttpPost("avatar")] + [Obsolete("This endpoint is deprecated. Use PUT /avatar instead.")] + public async Task PostAvatar([FromBody] UpdateAvatarRequestModel model) + { + return await PutAvatar(model); + } + [HttpGet("revision-date")] public async Task GetAccountRevisionDate() { @@ -430,7 +442,6 @@ public class AccountsController : Controller } [HttpDelete] - [HttpPost("delete")] public async Task Delete([FromBody] SecretVerificationRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -467,6 +478,13 @@ public class AccountsController : Controller throw new BadRequestException(ModelState); } + [HttpPost("delete")] + [Obsolete("This endpoint is deprecated. Use DELETE / instead.")] + public async Task PostDelete([FromBody] SecretVerificationRequestModel model) + { + await Delete(model); + } + [AllowAnonymous] [HttpPost("delete-recover")] public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model) @@ -638,7 +656,6 @@ public class AccountsController : Controller await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user); } - [HttpPost("verify-devices")] [HttpPut("verify-devices")] public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request) { @@ -654,6 +671,13 @@ public class AccountsController : Controller await _userService.SaveUserAsync(user); } + [HttpPost("verify-devices")] + [Obsolete("This endpoint is deprecated. Use PUT /verify-devices instead.")] + public async Task PostSetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request) + { + await SetUserVerifyDevicesAsync(request); + } + private async Task> GetOrganizationIdsClaimingUserAsync(Guid userId) { var organizationsClaimingUser = await _userService.GetOrganizationsClaimingUserAsync(userId); diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index 3f91bd6eea..c62b817905 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -31,7 +31,7 @@ public class AuthRequestsController( private readonly IAuthRequestService _authRequestService = authRequestService; [HttpGet("")] - public async Task> Get() + public async Task> GetAll() { var userId = _userService.GetProperUserId(User).Value; var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId); diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 53b57fe685..b849dc3e07 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -79,7 +79,6 @@ public class EmergencyAccessController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); @@ -92,14 +91,27 @@ public class EmergencyAccessController : Controller await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), user); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")] + public async Task Post(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model) + { + await Put(id, model); + } + [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(Guid id) { var userId = _userService.GetProperUserId(User); await _emergencyAccessService.DeleteAsync(id, userId.Value); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")] + public async Task PostDelete(Guid id) + { + await Delete(id); + } + [HttpPost("invite")] public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model) { @@ -136,7 +148,7 @@ public class EmergencyAccessController : Controller } [HttpPost("{id}/approve")] - public async Task Accept(Guid id) + public async Task Approve(Guid id) { var user = await _userService.GetUserByPrincipalAsync(User); await _emergencyAccessService.ApproveAsync(id, user); diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 4155489daa..886ed2cd20 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -110,7 +110,6 @@ public class TwoFactorController : Controller } [HttpPut("authenticator")] - [HttpPost("authenticator")] public async Task PutAuthenticator( [FromBody] UpdateTwoFactorAuthenticatorRequestModel model) { @@ -133,6 +132,14 @@ public class TwoFactorController : Controller return response; } + [HttpPost("authenticator")] + [Obsolete("This endpoint is deprecated. Use PUT /authenticator instead.")] + public async Task PostAuthenticator( + [FromBody] UpdateTwoFactorAuthenticatorRequestModel model) + { + return await PutAuthenticator(model); + } + [HttpDelete("authenticator")] public async Task DisableAuthenticator( [FromBody] TwoFactorAuthenticatorDisableRequestModel model) @@ -157,7 +164,6 @@ public class TwoFactorController : Controller } [HttpPut("yubikey")] - [HttpPost("yubikey")] public async Task PutYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model) { var user = await CheckAsync(model, true); @@ -174,6 +180,13 @@ public class TwoFactorController : Controller return response; } + [HttpPost("yubikey")] + [Obsolete("This endpoint is deprecated. Use PUT /yubikey instead.")] + public async Task PostYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model) + { + return await PutYubiKey(model); + } + [HttpPost("get-duo")] public async Task GetDuo([FromBody] SecretVerificationRequestModel model) { @@ -183,7 +196,6 @@ public class TwoFactorController : Controller } [HttpPut("duo")] - [HttpPost("duo")] public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); @@ -199,6 +211,13 @@ public class TwoFactorController : Controller return response; } + [HttpPost("duo")] + [Obsolete("This endpoint is deprecated. Use PUT /duo instead.")] + public async Task PostDuo([FromBody] UpdateTwoFactorDuoRequestModel model) + { + return await PutDuo(model); + } + [HttpPost("~/organizations/{id}/two-factor/get-duo")] public async Task GetOrganizationDuo(string id, [FromBody] SecretVerificationRequestModel model) @@ -217,7 +236,6 @@ public class TwoFactorController : Controller } [HttpPut("~/organizations/{id}/two-factor/duo")] - [HttpPost("~/organizations/{id}/two-factor/duo")] public async Task PutOrganizationDuo(string id, [FromBody] UpdateTwoFactorDuoRequestModel model) { @@ -243,6 +261,14 @@ public class TwoFactorController : Controller return response; } + [HttpPost("~/organizations/{id}/two-factor/duo")] + [Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/duo instead.")] + public async Task PostOrganizationDuo(string id, + [FromBody] UpdateTwoFactorDuoRequestModel model) + { + return await PutOrganizationDuo(id, model); + } + [HttpPost("get-webauthn")] public async Task GetWebAuthn([FromBody] SecretVerificationRequestModel model) { @@ -261,7 +287,6 @@ public class TwoFactorController : Controller } [HttpPut("webauthn")] - [HttpPost("webauthn")] public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) { var user = await CheckAsync(model, false); @@ -277,6 +302,13 @@ public class TwoFactorController : Controller return response; } + [HttpPost("webauthn")] + [Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")] + public async Task PostWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) + { + return await PutWebAuthn(model); + } + [HttpDelete("webauthn")] public async Task DeleteWebAuthn( [FromBody] TwoFactorWebAuthnDeleteRequestModel model) @@ -349,7 +381,6 @@ public class TwoFactorController : Controller } [HttpPut("email")] - [HttpPost("email")] public async Task PutEmail([FromBody] UpdateTwoFactorEmailRequestModel model) { var user = await CheckAsync(model, false); @@ -367,8 +398,14 @@ public class TwoFactorController : Controller return response; } + [HttpPost("email")] + [Obsolete("This endpoint is deprecated. Use PUT /email instead.")] + public async Task PostEmail([FromBody] UpdateTwoFactorEmailRequestModel model) + { + return await PutEmail(model); + } + [HttpPut("disable")] - [HttpPost("disable")] public async Task PutDisable([FromBody] TwoFactorProviderRequestModel model) { var user = await CheckAsync(model, false); @@ -377,8 +414,14 @@ public class TwoFactorController : Controller return response; } + [HttpPost("disable")] + [Obsolete("This endpoint is deprecated. Use PUT /disable instead.")] + public async Task PostDisable([FromBody] TwoFactorProviderRequestModel model) + { + return await PutDisable(model); + } + [HttpPut("~/organizations/{id}/two-factor/disable")] - [HttpPost("~/organizations/{id}/two-factor/disable")] public async Task PutOrganizationDisable(string id, [FromBody] TwoFactorProviderRequestModel model) { @@ -401,6 +444,14 @@ public class TwoFactorController : Controller return response; } + [HttpPost("~/organizations/{id}/two-factor/disable")] + [Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/disable instead.")] + public async Task PostOrganizationDisable(string id, + [FromBody] TwoFactorProviderRequestModel model) + { + return await PutOrganizationDisable(id, model); + } + [HttpPost("get-recover")] public async Task GetRecover([FromBody] SecretVerificationRequestModel model) { diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index 6d4e9c9fea..f037ab7034 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -102,7 +102,7 @@ public class CollectionsController : Controller } [HttpGet("")] - public async Task> Get(Guid orgId) + public async Task> GetAll(Guid orgId) { IEnumerable orgCollections; @@ -173,7 +173,6 @@ public class CollectionsController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model) { var collection = await _collectionRepository.GetByIdAsync(id); @@ -198,6 +197,13 @@ public class CollectionsController : Controller return new CollectionAccessDetailsResponseModel(collectionWithPermissions); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")] + public async Task Post(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model) + { + return await Put(orgId, id, model); + } + [HttpPost("bulk-access")] public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model) { @@ -222,7 +228,6 @@ public class CollectionsController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(Guid orgId, Guid id) { var collection = await _collectionRepository.GetByIdAsync(id); @@ -235,8 +240,14 @@ public class CollectionsController : Controller await _deleteCollectionCommand.DeleteAsync(collection); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")] + public async Task PostDelete(Guid orgId, Guid id) + { + await Delete(orgId, id); + } + [HttpDelete("")] - [HttpPost("delete")] public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model) { var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids); @@ -248,4 +259,11 @@ public class CollectionsController : Controller await _deleteCollectionCommand.DeleteManyAsync(collections); } + + [HttpPost("delete")] + [Obsolete("This endpoint is deprecated. Use DELETE / instead.")] + public async Task PostDeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model) + { + await DeleteMany(orgId, model); + } } diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index 07e8552268..1f2cda9cc4 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -75,7 +75,7 @@ public class DevicesController : Controller } [HttpGet("")] - public async Task> Get() + public async Task> GetAll() { var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value); @@ -99,7 +99,6 @@ public class DevicesController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(string id, [FromBody] DeviceRequestModel model) { var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value); @@ -114,8 +113,14 @@ public class DevicesController : Controller return response; } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")] + public async Task Post(string id, [FromBody] DeviceRequestModel model) + { + return await Put(id, model); + } + [HttpPut("{identifier}/keys")] - [HttpPost("{identifier}/keys")] public async Task PutKeys(string identifier, [FromBody] DeviceKeysRequestModel model) { var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value); @@ -130,6 +135,13 @@ public class DevicesController : Controller return response; } + [HttpPost("{identifier}/keys")] + [Obsolete("This endpoint is deprecated. Use PUT /{identifier}/keys instead.")] + public async Task PostKeys(string identifier, [FromBody] DeviceKeysRequestModel model) + { + return await PutKeys(identifier, model); + } + [HttpPost("{identifier}/retrieve-keys")] [Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")] public async Task GetDeviceKeys(string identifier) @@ -187,7 +199,6 @@ public class DevicesController : Controller } [HttpPut("identifier/{identifier}/token")] - [HttpPost("identifier/{identifier}/token")] public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model) { var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value); @@ -199,8 +210,14 @@ public class DevicesController : Controller await _deviceService.SaveAsync(model.ToDevice(device)); } + [HttpPost("identifier/{identifier}/token")] + [Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/token instead.")] + public async Task PostToken(string identifier, [FromBody] DeviceTokenRequestModel model) + { + await PutToken(identifier, model); + } + [HttpPut("identifier/{identifier}/web-push-auth")] - [HttpPost("identifier/{identifier}/web-push-auth")] public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model) { var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value); @@ -216,9 +233,15 @@ public class DevicesController : Controller ); } + [HttpPost("identifier/{identifier}/web-push-auth")] + [Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/web-push-auth instead.")] + public async Task PostWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model) + { + await PutWebPushAuth(identifier, model); + } + [AllowAnonymous] [HttpPut("identifier/{identifier}/clear-token")] - [HttpPost("identifier/{identifier}/clear-token")] public async Task PutClearToken(string identifier) { var device = await _deviceRepository.GetByIdentifierAsync(identifier); @@ -230,8 +253,15 @@ public class DevicesController : Controller await _deviceService.ClearTokenAsync(device); } + [AllowAnonymous] + [HttpPost("identifier/{identifier}/clear-token")] + [Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/clear-token instead.")] + public async Task PostClearToken(string identifier) + { + await PutClearToken(identifier); + } + [HttpDelete("{id}")] - [HttpPost("{id}/deactivate")] public async Task Deactivate(string id) { var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value); @@ -243,17 +273,24 @@ public class DevicesController : Controller await _deviceService.DeactivateAsync(device); } + [HttpPost("{id}/deactivate")] + [Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")] + public async Task PostDeactivate(string id) + { + await Deactivate(id); + } + [AllowAnonymous] [HttpGet("knowndevice")] public async Task GetByIdentifierQuery( [Required][FromHeader(Name = "X-Request-Email")] string Email, [Required][FromHeader(Name = "X-Device-Identifier")] string DeviceIdentifier) - => await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier); + => await GetByEmailAndIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier); [Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")] [AllowAnonymous] [HttpGet("knowndevice/{email}/{identifier}")] - public async Task GetByIdentifier(string email, string identifier) + public async Task GetByEmailAndIdentifier(string email, string identifier) { if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(identifier)) { diff --git a/src/Api/Controllers/InfoController.cs b/src/Api/Controllers/InfoController.cs index edfd18c79e..590a3006c0 100644 --- a/src/Api/Controllers/InfoController.cs +++ b/src/Api/Controllers/InfoController.cs @@ -6,12 +6,18 @@ namespace Bit.Api.Controllers; public class InfoController : Controller { [HttpGet("~/alive")] - [HttpGet("~/now")] public DateTime GetAlive() { return DateTime.UtcNow; } + [HttpGet("~/now")] + [Obsolete("This endpoint is deprecated. Use GET /alive instead.")] + public DateTime GetNow() + { + return GetAlive(); + } + [HttpGet("~/version")] public JsonResult GetVersion() { diff --git a/src/Api/Controllers/SettingsController.cs b/src/Api/Controllers/SettingsController.cs index 8489b137e8..e872eeeeac 100644 --- a/src/Api/Controllers/SettingsController.cs +++ b/src/Api/Controllers/SettingsController.cs @@ -32,7 +32,6 @@ public class SettingsController : Controller } [HttpPut("domains")] - [HttpPost("domains")] public async Task PutDomains([FromBody] UpdateDomainsRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -46,4 +45,11 @@ public class SettingsController : Controller var response = new DomainsResponseModel(user); return response; } + + [HttpPost("domains")] + [Obsolete("This endpoint is deprecated. Use PUT /domains instead.")] + public async Task PostDomains([FromBody] UpdateDomainsRequestModel model) + { + return await PutDomains(model); + } } diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index aa2710c42a..0d8c3dec38 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -82,15 +82,7 @@ public static class ServiceCollectionExtensions config.DescribeAllParametersInCamelCase(); // config.UseReferencedDefinitionsForEnums(); - config.SchemaFilter(); - config.SchemaFilter(); - - // These two filters require debug symbols/git, so only add them in development mode - if (environment.IsDevelopment()) - { - config.DocumentFilter(); - config.OperationFilter(); - } + config.InitializeSwaggerFilters(environment); var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml"); config.IncludeXmlComments(apiFilePath, true); diff --git a/src/Identity/Controllers/InfoController.cs b/src/Identity/Controllers/InfoController.cs index 05cf3f2363..79dfd99c44 100644 --- a/src/Identity/Controllers/InfoController.cs +++ b/src/Identity/Controllers/InfoController.cs @@ -6,12 +6,18 @@ namespace Bit.Identity.Controllers; public class InfoController : Controller { [HttpGet("~/alive")] - [HttpGet("~/now")] public DateTime GetAlive() { return DateTime.UtcNow; } + [HttpGet("~/now")] + [Obsolete("This endpoint is deprecated. Use GET /alive instead.")] + public DateTime GetNow() + { + return GetAlive(); + } + [HttpGet("~/version")] public JsonResult GetVersion() { diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index ae628197e8..8da31d87d6 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -66,15 +66,7 @@ public class Startup services.AddSwaggerGen(config => { - config.SchemaFilter(); - config.SchemaFilter(); - - // These two filters require debug symbols/git, so only add them in development mode - if (Environment.IsDevelopment()) - { - config.DocumentFilter(); - config.OperationFilter(); - } + config.InitializeSwaggerFilters(Environment); config.SwaggerDoc("v1", new OpenApiInfo { Title = "Bitwarden Identity", Version = "v1" }); }); diff --git a/src/SharedWeb/Swagger/ActionNameOperationFilter.cs b/src/SharedWeb/Swagger/ActionNameOperationFilter.cs new file mode 100644 index 0000000000..b76e8864ba --- /dev/null +++ b/src/SharedWeb/Swagger/ActionNameOperationFilter.cs @@ -0,0 +1,25 @@ +using System.Text.Json; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +/// +/// Adds the action name (function name) as an extension to each operation in the Swagger document. +/// This can be useful for the code generation process, to generate more meaningful names for operations. +/// Note that we add both the original action name and a snake_case version, as the codegen templates +/// cannot do case conversions. +/// +public class ActionNameOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (!context.ApiDescription.ActionDescriptor.RouteValues.TryGetValue("action", out var action)) return; + if (string.IsNullOrEmpty(action)) return; + + operation.Extensions.Add("x-action-name", new OpenApiString(action)); + // We can't do case changes in the codegen templates, so we also add the snake_case version of the action name + operation.Extensions.Add("x-action-name-snake-case", new OpenApiString(JsonNamingPolicy.SnakeCaseLower.ConvertName(action))); + } +} diff --git a/src/SharedWeb/Swagger/CheckDuplicateOperationIdsDocumentFilter.cs b/src/SharedWeb/Swagger/CheckDuplicateOperationIdsDocumentFilter.cs new file mode 100644 index 0000000000..3079a9171a --- /dev/null +++ b/src/SharedWeb/Swagger/CheckDuplicateOperationIdsDocumentFilter.cs @@ -0,0 +1,80 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +/// +/// Checks for duplicate operation IDs in the Swagger document, and throws an error if any are found. +/// Operation IDs must be unique across the entire Swagger document according to the OpenAPI specification, +/// but we use controller action names to generate them, which can lead to duplicates if a Controller function +/// has multiple HTTP methods or if a Controller has overloaded functions. +/// +public class CheckDuplicateOperationIdsDocumentFilter(bool printDuplicates = true) : IDocumentFilter +{ + public bool PrintDuplicates { get; } = printDuplicates; + + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + var operationIdMap = new Dictionary>(); + + foreach (var (path, pathItem) in swaggerDoc.Paths) + { + foreach (var operation in pathItem.Operations) + { + if (!operationIdMap.TryGetValue(operation.Value.OperationId, out var list)) + { + list = []; + operationIdMap[operation.Value.OperationId] = list; + } + + list.Add((path, pathItem, operation.Key, operation.Value)); + + } + } + + // Find duplicates + var duplicates = operationIdMap.Where((kvp) => kvp.Value.Count > 1).ToList(); + if (duplicates.Count > 0) + { + if (PrintDuplicates) + { + Console.WriteLine($"\n######## Duplicate operationIds found in the schema ({duplicates.Count} found) ########\n"); + + Console.WriteLine("## Common causes of duplicate operation IDs:"); + Console.WriteLine("- Multiple HTTP methods (GET, POST, etc.) on the same controller function"); + Console.WriteLine(" Solution: Split the methods into separate functions, and if appropiate, mark the deprecated ones with [Obsolete]"); + Console.WriteLine(); + Console.WriteLine("- Overloaded controller functions with the same name"); + Console.WriteLine(" Solution: Rename the overloaded functions to have unique names, or combine them into a single function with optional parameters"); + Console.WriteLine(); + + Console.WriteLine("## The duplicate operation IDs are:"); + + foreach (var (operationId, duplicate) in duplicates) + { + Console.WriteLine($"- operationId: {operationId}"); + foreach (var (path, pathItem, method, operation) in duplicate) + { + Console.Write($" {method.ToString().ToUpper()} {path}"); + + + if (operation.Extensions.TryGetValue("x-source-file", out var sourceFile) && operation.Extensions.TryGetValue("x-source-line", out var sourceLine)) + { + var sourceFileString = ((Microsoft.OpenApi.Any.OpenApiString)sourceFile).Value; + var sourceLineString = ((Microsoft.OpenApi.Any.OpenApiInteger)sourceLine).Value; + + Console.WriteLine($" {sourceFileString}:{sourceLineString}"); + } + else + { + Console.WriteLine(); + } + } + Console.WriteLine("\n"); + } + } + + throw new InvalidOperationException($"Duplicate operation IDs found in Swagger schema"); + } + } +} diff --git a/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs b/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs new file mode 100644 index 0000000000..60803705d6 --- /dev/null +++ b/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Bit.SharedWeb.Swagger; + +public static class SwaggerGenOptionsExt +{ + + public static void InitializeSwaggerFilters( + this SwaggerGenOptions config, IWebHostEnvironment environment) + { + config.SchemaFilter(); + config.SchemaFilter(); + + config.OperationFilter(); + + // Set the operation ID to the name of the controller followed by the name of the function. + // Note that the "Controller" suffix for the controllers, and the "Async" suffix for the actions + // are removed already, so we don't need to do that ourselves. + // TODO(Dani): This is disabled until we remove all the duplicate operation IDs. + // config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}"); + // config.DocumentFilter(); + + // These two filters require debug symbols/git, so only add them in development mode + if (environment.IsDevelopment()) + { + config.DocumentFilter(); + config.OperationFilter(); + } + } +} diff --git a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs index 828911f6bd..1b8e7aba8e 100644 --- a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs @@ -43,7 +43,7 @@ public class AuthRequestsControllerTests .Returns([authRequest]); // Act - var result = await sutProvider.Sut.Get(); + var result = await sutProvider.Sut.GetAll(); // Assert Assert.NotNull(result); diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index 540d23f98b..bed483f83a 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -73,7 +73,7 @@ public class DevicesControllerTest _deviceRepositoryMock.GetManyByUserIdWithDeviceAuth(userId).Returns(devicesWithPendingAuthData); // Act - var result = await _sut.Get(); + var result = await _sut.GetAll(); // Assert Assert.NotNull(result); @@ -94,6 +94,6 @@ public class DevicesControllerTest _userServiceMock.GetProperUserId(Arg.Any()).Returns((Guid?)null); // Act & Assert - await Assert.ThrowsAsync(() => _sut.Get()); + await Assert.ThrowsAsync(() => _sut.GetAll()); } } diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index a3d34efb63..33b7e20327 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -177,7 +177,7 @@ public class CollectionsControllerTests .GetManySharedCollectionsByOrganizationIdAsync(organization.Id) .Returns(collections); - var response = await sutProvider.Sut.Get(organization.Id); + var response = await sutProvider.Sut.GetAll(organization.Id); await sutProvider.GetDependency().Received(1).GetManySharedCollectionsByOrganizationIdAsync(organization.Id); @@ -219,7 +219,7 @@ public class CollectionsControllerTests .GetManyByUserIdAsync(userId) .Returns(collections); - var result = await sutProvider.Sut.Get(organization.Id); + var result = await sutProvider.Sut.GetAll(organization.Id); await sutProvider.GetDependency().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id); await sutProvider.GetDependency().Received(1).GetManyByUserIdAsync(userId); diff --git a/test/SharedWeb.Test/ActionNameOperationFilterTest.cs b/test/SharedWeb.Test/ActionNameOperationFilterTest.cs new file mode 100644 index 0000000000..c798adea8c --- /dev/null +++ b/test/SharedWeb.Test/ActionNameOperationFilterTest.cs @@ -0,0 +1,67 @@ +using Bit.SharedWeb.Swagger; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class ActionNameOperationFilterTest +{ + [Fact] + public void WithValidActionNameAddsActionNameExtensions() + { + // Arrange + var operation = new OpenApiOperation(); + var actionDescriptor = new ActionDescriptor(); + actionDescriptor.RouteValues["action"] = "GetUsers"; + + var apiDescription = new ApiDescription + { + ActionDescriptor = actionDescriptor + }; + + var context = new OperationFilterContext(apiDescription, null, null, null); + var filter = new ActionNameOperationFilter(); + + // Act + filter.Apply(operation, context); + + // Assert + Assert.True(operation.Extensions.ContainsKey("x-action-name")); + Assert.True(operation.Extensions.ContainsKey("x-action-name-snake-case")); + + var actionNameExt = operation.Extensions["x-action-name"] as OpenApiString; + var actionNameSnakeCaseExt = operation.Extensions["x-action-name-snake-case"] as OpenApiString; + + Assert.NotNull(actionNameExt); + Assert.NotNull(actionNameSnakeCaseExt); + Assert.Equal("GetUsers", actionNameExt.Value); + Assert.Equal("get_users", actionNameSnakeCaseExt.Value); + } + + [Fact] + public void WithMissingActionRouteValueDoesNotAddExtensions() + { + // Arrange + var operation = new OpenApiOperation(); + var actionDescriptor = new ActionDescriptor(); + // Not setting the "action" route value at all + + var apiDescription = new ApiDescription + { + ActionDescriptor = actionDescriptor + }; + + var context = new OperationFilterContext(apiDescription, null, null, null); + var filter = new ActionNameOperationFilter(); + + // Act + filter.Apply(operation, context); + + // Assert + Assert.False(operation.Extensions.ContainsKey("x-action-name")); + Assert.False(operation.Extensions.ContainsKey("x-action-name-snake-case")); + } +} diff --git a/test/SharedWeb.Test/CheckDuplicateOperationIdsDocumentFilterTest.cs b/test/SharedWeb.Test/CheckDuplicateOperationIdsDocumentFilterTest.cs new file mode 100644 index 0000000000..7b7c5771d3 --- /dev/null +++ b/test/SharedWeb.Test/CheckDuplicateOperationIdsDocumentFilterTest.cs @@ -0,0 +1,84 @@ +using Bit.SharedWeb.Swagger; +using Microsoft.AspNetCore.Mvc; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class UniqueOperationIdsController : ControllerBase +{ + [HttpGet("unique-get")] + public void UniqueGetAction() { } + + [HttpPost("unique-post")] + public void UniquePostAction() { } +} + +public class OverloadedOperationIdsController : ControllerBase +{ + [HttpPut("another-duplicate")] + public void AnotherDuplicateAction() { } + + [HttpPatch("another-duplicate/{id}")] + public void AnotherDuplicateAction(int id) { } +} + +public class MultipleHttpMethodsController : ControllerBase +{ + [HttpGet("multi-method")] + [HttpPost("multi-method")] + [HttpPut("multi-method")] + public void MultiMethodAction() { } +} + +public class CheckDuplicateOperationIdsDocumentFilterTest +{ + [Fact] + public void UniqueOperationIdsDoNotThrowException() + { + // Arrange + var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(UniqueOperationIdsController)); + var filter = new CheckDuplicateOperationIdsDocumentFilter(); + filter.Apply(swaggerDoc, context); + // Act & Assert + var exception = Record.Exception(() => filter.Apply(swaggerDoc, context)); + Assert.Null(exception); + } + + [Fact] + public void DuplicateOperationIdsThrowInvalidOperationException() + { + // Arrange + var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(OverloadedOperationIdsController)); + var filter = new CheckDuplicateOperationIdsDocumentFilter(false); + + // Act & Assert + var exception = Assert.Throws(() => filter.Apply(swaggerDoc, context)); + Assert.Contains("Duplicate operation IDs found in Swagger schema", exception.Message); + } + + [Fact] + public void MultipleHttpMethodsThrowInvalidOperationException() + { + // Arrange + var (swaggerDoc, context) = SwaggerDocUtil.CreateDocFromControllers(typeof(MultipleHttpMethodsController)); + var filter = new CheckDuplicateOperationIdsDocumentFilter(false); + + // Act & Assert + var exception = Assert.Throws(() => filter.Apply(swaggerDoc, context)); + Assert.Contains("Duplicate operation IDs found in Swagger schema", exception.Message); + } + + [Fact] + public void EmptySwaggerDocDoesNotThrowException() + { + // Arrange + var swaggerDoc = new OpenApiDocument { Paths = [] }; + var context = new DocumentFilterContext([], null, null); + var filter = new CheckDuplicateOperationIdsDocumentFilter(false); + + // Act & Assert + var exception = Record.Exception(() => filter.Apply(swaggerDoc, context)); + Assert.Null(exception); + } +} diff --git a/test/SharedWeb.Test/SharedWeb.Test.csproj b/test/SharedWeb.Test/SharedWeb.Test.csproj index 8ae7a56a99..c631ac9227 100644 --- a/test/SharedWeb.Test/SharedWeb.Test.csproj +++ b/test/SharedWeb.Test/SharedWeb.Test.csproj @@ -9,6 +9,7 @@ all + diff --git a/test/SharedWeb.Test/SwaggerDocUtil.cs b/test/SharedWeb.Test/SwaggerDocUtil.cs new file mode 100644 index 0000000000..45a3033dec --- /dev/null +++ b/test/SharedWeb.Test/SwaggerDocUtil.cs @@ -0,0 +1,85 @@ +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using NSubstitute; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SharedWeb.Test; + +public class SwaggerDocUtil +{ + /// + /// Creates an OpenApiDocument and DocumentFilterContext from the specified controller type by setting up + /// a minimal service collection and using the SwaggerProvider to generate the document. + /// + public static (OpenApiDocument, DocumentFilterContext) CreateDocFromControllers(params Type[] controllerTypes) + { + if (controllerTypes.Length == 0) + { + throw new ArgumentException("At least one controller type must be provided", nameof(controllerTypes)); + } + + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(Substitute.For()); + services.AddControllers() + .ConfigureApplicationPartManager(manager => + { + // Clear existing parts and feature providers + manager.ApplicationParts.Clear(); + manager.FeatureProviders.Clear(); + + // Add a custom feature provider that only includes the specific controller types + manager.FeatureProviders.Add(new MultipleControllerFeatureProvider(controllerTypes)); + + // Add assembly parts for all unique assemblies containing the controllers + foreach (var assembly in controllerTypes.Select(t => t.Assembly).Distinct()) + { + manager.ApplicationParts.Add(new AssemblyPart(assembly)); + } + }); + services.AddSwaggerGen(config => + { + config.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}"); + }); + var serviceProvider = services.BuildServiceProvider(); + + // Get API descriptions + var allApiDescriptions = serviceProvider.GetRequiredService() + .ApiDescriptionGroups.Items + .SelectMany(group => group.Items) + .ToList(); + + if (allApiDescriptions.Count == 0) + { + throw new InvalidOperationException("No API descriptions found for controller, ensure your controllers are defined correctly (public, not nested, inherit from ControllerBase, etc.)"); + } + + // Generate the swagger document and context + var document = serviceProvider.GetRequiredService().GetSwagger("v1"); + var schemaGenerator = serviceProvider.GetRequiredService(); + var context = new DocumentFilterContext(allApiDescriptions, schemaGenerator, new SchemaRepository()); + + return (document, context); + } +} + +public class MultipleControllerFeatureProvider(params Type[] controllerTypes) : ControllerFeatureProvider +{ + private readonly HashSet _allowedControllerTypes = [.. controllerTypes]; + + protected override bool IsController(TypeInfo typeInfo) + { + return _allowedControllerTypes.Contains(typeInfo.AsType()) + && typeInfo.IsClass + && !typeInfo.IsAbstract + && typeof(ControllerBase).IsAssignableFrom(typeInfo); + } +} From 53e5ddb1a719aa4ca23004196b569c12c0ac6722 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Tue, 2 Sep 2025 12:44:28 -0400 Subject: [PATCH 182/326] fix(inactive-user-server-notification): [PM-25130] Inactive User Server Notify - Added feature flag. (#6270) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 352daee862..ce18706bd4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -187,6 +187,7 @@ public static class FeatureFlagKeys public const string PersistPopupView = "persist-popup-view"; public const string IpcChannelFramework = "ipc-channel-framework"; public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; + public const string PushNotificationsWhenInactive = "pm-25130-receive-push-notifications-for-inactive-users"; /* Tools Team */ public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; From a5bed5dcaab3aba3aba588531561add60927b273 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:02:02 -0500 Subject: [PATCH 183/326] [PM-25384] Add feature flag (#6271) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ce18706bd4..393ab15e4c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -163,6 +163,7 @@ public static class FeatureFlagKeys public const string UserSdkForDecryption = "use-sdk-for-decryption"; 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"; /* Mobile Team */ public const string NativeCarouselFlow = "native-carousel-flow"; From d2d3e0f11b6950b172dd3e16ac1f47f310e9ec50 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Tue, 2 Sep 2025 16:48:57 -0400 Subject: [PATCH 184/326] [PM-22678] Send email otp authentication method (#6255) feat(auth): email OTP validation, and generalize authentication interface - Generalized send authentication method interface - Made validate method async - Added email mail support for Handlebars - Modified email templates to match future implementation fix(auth): update constants, naming conventions, and error handling - Renamed constants for clarity - Updated claims naming convention - Fixed error message generation - Added customResponse for Rust consumption test(auth): add and fix tests for validators and email - Added tests for SendEmailOtpRequestValidator - Updated tests for SendAccessGrantValidator chore: apply dotnet formatting --- .../SendAccessClaimsPrincipalExtensions.cs | 6 +- src/Core/Identity/Claims.cs | 7 +- .../Auth/SendAccessEmailOtpEmail.html.hbs | 28 ++ .../Auth/SendAccessEmailOtpEmail.text.hbs | 9 + .../Mail/Auth/DefaultEmailOtpViewModel.cs | 12 + src/Core/Services/IMailService.cs | 1 + .../Implementations/HandlebarsMailService.cs | 21 ++ .../NoopImplementations/NoopMailService.cs | 5 + src/Identity/IdentityServer/ApiResources.cs | 2 +- .../ISendAuthenticationMethodValidator.cs | 15 + .../ISendPasswordRequestValidator.cs | 16 - .../SendAccess/SendAccessConstants.cs | 37 ++- .../SendAccess/SendAccessGrantValidator.cs | 38 +-- .../SendEmailOtpRequestValidator.cs | 134 ++++++++ .../SendPasswordRequestValidator.cs | 16 +- .../Utilities/ServiceCollectionExtensions.cs | 4 +- ...endAccessClaimsPrincipalExtensionsTests.cs | 8 +- .../Services/HandlebarsMailServiceTests.cs | 15 +- ...endAccessGrantValidatorIntegrationTests.cs | 4 +- ...EmailOtpReqestValidatorIntegrationTests.cs | 256 +++++++++++++++ .../SendAccessGrantValidatorTests.cs | 30 +- .../SendEmailOtpRequestValidatorTests.cs | 310 ++++++++++++++++++ .../SendPasswordRequestValidatorTests.cs | 297 +++++++++++++++++ .../SendPasswordRequestValidatorTests.cs | 32 +- 24 files changed, 1213 insertions(+), 90 deletions(-) create mode 100644 src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.text.hbs create mode 100644 src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/ISendAuthenticationMethodValidator.cs delete mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs create mode 100644 test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs rename test/Identity.Test/IdentityServer/{ => SendAccess}/SendAccessGrantValidatorTests.cs (90%) create mode 100644 test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs create mode 100644 test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs diff --git a/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs index 1feadaf081..7ae7355ba4 100644 --- a/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs +++ b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs @@ -9,12 +9,12 @@ public static class SendAccessClaimsPrincipalExtensions { ArgumentNullException.ThrowIfNull(user); - var sendIdClaim = user.FindFirst(Claims.SendId) - ?? throw new InvalidOperationException("Send ID claim not found."); + var sendIdClaim = user.FindFirst(Claims.SendAccessClaims.SendId) + ?? throw new InvalidOperationException("send_id claim not found."); if (!Guid.TryParse(sendIdClaim.Value, out var sendGuid)) { - throw new InvalidOperationException("Invalid Send ID claim value."); + throw new InvalidOperationException("Invalid send_id claim value."); } return sendGuid; diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs index ef3d5e450c..39a036f3f9 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Identity/Claims.cs @@ -39,6 +39,9 @@ public static class Claims public const string ManageResetPassword = "manageresetpassword"; public const string ManageScim = "managescim"; } - - public const string SendId = "send_id"; + public static class SendAccessClaims + { + public const string SendId = "send_id"; + public const string Email = "send_email"; + } } diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.html.hbs new file mode 100644 index 0000000000..5bf1f24218 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.html.hbs @@ -0,0 +1,28 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + + + + +
+ Verify your email to access this Bitwarden Send. +
+
+ Your verification code is: {{Token}} +
+
+ This code can only be used once and expires in 5 minutes. After that you'll need to verify your email again. +
+
+
+ {{TheDate}} at {{TheTime}} {{TimeZone}} +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.text.hbs new file mode 100644 index 0000000000..f83008c30b --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Auth/SendAccessEmailOtpEmail.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 5 minutes. After that you'll need to verify your email again. + +Date : {{TheDate}} at {{TheTime}} {{TimeZone}} +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs new file mode 100644 index 0000000000..5faf550e60 --- /dev/null +++ b/src/Core/Models/Mail/Auth/DefaultEmailOtpViewModel.cs @@ -0,0 +1,12 @@ +namespace Bit.Core.Models.Mail.Auth; + +/// +/// Send email OTP view model +/// +public class DefaultEmailOtpViewModel : BaseMailModel +{ + public string? Token { get; set; } + public string? TheDate { get; set; } + public string? TheTime { get; set; } + public string? TimeZone { get; set; } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 32aaac84b7..a38328dc9d 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -30,6 +30,7 @@ public interface IMailService Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); 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); 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 f06a37fa3b..394b5c5125 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -15,6 +15,7 @@ using Bit.Core.Billing.Models.Mail; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Mail; +using Bit.Core.Models.Mail.Auth; using Bit.Core.Models.Mail.Billing; using Bit.Core.Models.Mail.FamiliesForEnterprise; using Bit.Core.Models.Mail.Provider; @@ -199,6 +200,26 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendSendEmailOtpEmailAsync(string email, string token, string subject) + { + var message = CreateDefaultMessage(subject, email); + var requestDateTime = DateTime.UtcNow; + var model = new DefaultEmailOtpViewModel + { + Token = token, + TheDate = requestDateTime.ToLongDateString(), + TheTime = requestDateTime.ToShortTimeString(), + TimeZone = _utcTimeZoneDisplay, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + }; + await AddMessageContentAsync(message, "Auth.SendAccessEmailOtpEmail", 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 5847aaf929..bc73fb5398 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -93,6 +93,11 @@ public class NoopMailService : IMailService return Task.FromResult(0); } + public Task SendSendEmailOtpEmailAsync(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/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index eea53734cb..61f3dd10ba 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -29,7 +29,7 @@ public class ApiResources }), new(ApiScopes.ApiSendAccess, [ JwtClaimTypes.Subject, - Claims.SendId + Claims.SendAccessClaims.SendId ]), new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }), new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }), diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendAuthenticationMethodValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendAuthenticationMethodValidator.cs new file mode 100644 index 0000000000..1ffb68ceca --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendAuthenticationMethodValidator.cs @@ -0,0 +1,15 @@ +using Bit.Core.Tools.Models.Data; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +public interface ISendAuthenticationMethodValidator where T : SendAuthenticationMethod +{ + /// + /// + /// request context + /// SendAuthenticationRecord that contains the information to be compared against the context + /// the sendId being accessed + /// returns the result of the validation; A failed result will be an error a successful will contain the claims and a success + Task ValidateRequestAsync(ExtensionGrantValidationContext context, T authMethod, Guid sendId); +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs deleted file mode 100644 index a6f33175bd..0000000000 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/ISendPasswordRequestValidator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Tools.Models.Data; -using Duende.IdentityServer.Validation; - -namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; - -public interface ISendPasswordRequestValidator -{ - /// - /// Validates the send password hash against the client hashed password. - /// If this method fails then it will automatically set the context.Result to an invalid grant result. - /// - /// request context - /// resource password authentication method containing the hash of the Send being retrieved - /// returns the result of the validation; A failed result will be an error a successful will contain the claims and a success - GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId); -} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs index 952f4146ed..fae7ba4215 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs @@ -1,4 +1,5 @@ -using Duende.IdentityServer.Validation; +using Bit.Core.Auth.Identity.TokenProviders; +using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; @@ -34,7 +35,7 @@ public static class SendAccessConstants public static class GrantValidatorResults { /// - /// The sendId is valid and the request is well formed. + /// The sendId is valid and the request is well formed. Not returned in any response. /// public const string ValidSendGuid = "valid_send_guid"; /// @@ -66,8 +67,40 @@ public static class SendAccessConstants /// public const string EmailRequired = "email_required"; /// + /// Represents the error code indicating that an email address is invalid. + /// + public const string EmailInvalid = "email_invalid"; + /// /// Represents the status indicating that both email and OTP are required, and the OTP has been sent. /// public const string EmailOtpSent = "email_and_otp_required_otp_sent"; + /// + /// Represents the status indicating that both email and OTP are required, and the OTP is invalid. + /// + public const string EmailOtpInvalid = "otp_invalid"; + /// + /// For what ever reason the OTP was not able to be generated + /// + public const string OtpGenerationFailed = "otp_generation_failed"; + } + + /// + /// These are the constants for the OTP token that is generated during the email otp authentication process. + /// These items are required by to aid in the creation of a unique lookup key. + /// Look up key format is: {TokenProviderName}_{Purpose}_{TokenUniqueIdentifier} + /// + public static class OtpToken + { + public const string TokenProviderName = "send_access"; + public const string Purpose = "email_otp"; + /// + /// This will be send_id {0} and email {1} + /// + public const string TokenUniqueIdentifier = "{0}_{1}"; + } + + public static class OtpEmail + { + public const string Subject = "Your Bitwarden Send verification code is {0}"; } } diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs index 7cfa2acd2a..5fe0b7b724 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -13,7 +13,8 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; public class SendAccessGrantValidator( ISendAuthenticationQuery _sendAuthenticationQuery, - ISendPasswordRequestValidator _sendPasswordRequestValidator, + ISendAuthenticationMethodValidator _sendPasswordRequestValidator, + ISendAuthenticationMethodValidator _sendEmailOtpRequestValidator, IFeatureService _featureService) : IExtensionGrantValidator { @@ -61,16 +62,14 @@ public class SendAccessGrantValidator( // automatically issue access token context.Result = BuildBaseSuccessResult(sendIdGuid); return; - case ResourcePassword rp: - // Validate if the password is correct, or if we need to respond with a 400 stating a password has is required - context.Result = _sendPasswordRequestValidator.ValidateSendPassword(context, rp, sendIdGuid); + // Validate if the password is correct, or if we need to respond with a 400 stating a password is invalid or required. + context.Result = await _sendPasswordRequestValidator.ValidateRequestAsync(context, rp, sendIdGuid); return; case EmailOtp eo: - // TODO PM-22678: We will either send the OTP here or validate it based on if otp exists in the request. - // SendOtpToEmail(eo.Emails) or ValidateOtp(eo.Emails); - // break; - + // Validate if the request has the correct email and OTP. If not, respond with a 400 and information about the failure. + context.Result = await _sendEmailOtpRequestValidator.ValidateRequestAsync(context, eo, sendIdGuid); + return; default: // shouldn’t ever hit this throw new InvalidOperationException($"Unknown auth method: {method.GetType()}"); @@ -114,28 +113,27 @@ public class SendAccessGrantValidator( /// /// Builds an error result for the specified error type. /// - /// The error type. + /// This error is a constant string from /// The error result. private static GrantValidationResult BuildErrorResult(string error) { + var customResponse = new Dictionary + { + { SendAccessConstants.SendAccessError, error } + }; + return error switch { // Request is the wrong shape SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult( TokenRequestErrors.InvalidRequest, - errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.MissingSendId], - new Dictionary - { - { SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.MissingSendId} - }), + errorDescription: _sendGrantValidatorErrorDescriptions[error], + customResponse), // Request is correct shape but data is bad SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult( TokenRequestErrors.InvalidGrant, - errorDescription: _sendGrantValidatorErrorDescriptions[SendAccessConstants.GrantValidatorResults.InvalidSendId], - new Dictionary - { - { SendAccessConstants.SendAccessError, SendAccessConstants.GrantValidatorResults.InvalidSendId } - }), + errorDescription: _sendGrantValidatorErrorDescriptions[error], + customResponse), // should never get here _ => new GrantValidationResult(TokenRequestErrors.InvalidRequest) }; @@ -145,7 +143,7 @@ public class SendAccessGrantValidator( { var claims = new List { - new(Claims.SendId, sendId.ToString()), + new(Claims.SendAccessClaims.SendId, sendId.ToString()), new(Claims.Type, IdentityClientType.Send.ToString()) }; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs new file mode 100644 index 0000000000..e26556eb80 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs @@ -0,0 +1,134 @@ +using System.Security.Claims; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Identity; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Identity.IdentityServer.Enums; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +public class SendEmailOtpRequestValidator( + IOtpTokenProvider otpTokenProvider, + IMailService mailService) : ISendAuthenticationMethodValidator +{ + + /// + /// static object that contains the error messages for the SendEmailOtpRequestValidator. + /// + private static readonly Dictionary _sendEmailOtpValidatorErrorDescriptions = new() + { + { SendAccessConstants.EmailOtpValidatorResults.EmailRequired, $"{SendAccessConstants.TokenRequest.Email} is required." }, + { SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent, "email otp sent." }, + { SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, $"{SendAccessConstants.TokenRequest.Email} is invalid." }, + { SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid, $"{SendAccessConstants.TokenRequest.Email} otp is invalid." }, + }; + + public async Task ValidateRequestAsync(ExtensionGrantValidationContext context, EmailOtp authMethod, Guid sendId) + { + var request = context.Request.Raw; + // get email + var email = request.Get(SendAccessConstants.TokenRequest.Email); + + // It is an invalid request if the email is missing which indicated bad shape. + if (string.IsNullOrEmpty(email)) + { + // Request is the wrong shape and doesn't contain an email field. + return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired); + } + + // email must be in the list of emails in the EmailOtp array + if (!authMethod.Emails.Contains(email)) + { + return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid); + } + + // get otp from request + var requestOtp = request.Get(SendAccessConstants.TokenRequest.Otp); + var uniqueIdentifierForTokenCache = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email); + if (string.IsNullOrEmpty(requestOtp)) + { + // Since the request doesn't have an OTP, generate one + var token = await otpTokenProvider.GenerateTokenAsync( + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + uniqueIdentifierForTokenCache); + + // Verify that the OTP is generated + if (string.IsNullOrEmpty(token)) + { + return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed); + } + + await mailService.SendSendEmailOtpEmailAsync( + email, + token, + string.Format(SendAccessConstants.OtpEmail.Subject, token)); + return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent); + } + + // validate request otp + var otpResult = await otpTokenProvider.ValidateTokenAsync( + requestOtp, + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + uniqueIdentifierForTokenCache); + + // If OTP is invalid return error result + if (!otpResult) + { + return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid); + } + + return BuildSuccessResult(sendId, email!); + } + + private static GrantValidationResult BuildErrorResult(string error) + { + switch (error) + { + case SendAccessConstants.EmailOtpValidatorResults.EmailRequired: + case SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent: + return new GrantValidationResult(TokenRequestErrors.InvalidRequest, + errorDescription: _sendEmailOtpValidatorErrorDescriptions[error], + new Dictionary + { + { SendAccessConstants.SendAccessError, error } + }); + case SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid: + case SendAccessConstants.EmailOtpValidatorResults.EmailInvalid: + return new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + errorDescription: _sendEmailOtpValidatorErrorDescriptions[error], + new Dictionary + { + { SendAccessConstants.SendAccessError, error } + }); + default: + return new GrantValidationResult( + TokenRequestErrors.InvalidRequest, + errorDescription: error); + } + } + + /// + /// Builds a successful validation result for the Send password send_access grant. + /// + /// Guid of the send being accessed. + /// successful grant validation result + private static GrantValidationResult BuildSuccessResult(Guid sendId, string email) + { + var claims = new List + { + new(Claims.SendAccessClaims.SendId, sendId.ToString()), + new(Claims.SendAccessClaims.Email, email), + new(Claims.Type, IdentityClientType.Send.ToString()) + }; + + return new GrantValidationResult( + subject: sendId.ToString(), + authenticationMethod: CustomGrantTypes.SendAccess, + claims: claims); + } +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs index 3449b4cb56..4eade01a49 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs @@ -8,7 +8,7 @@ using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; -public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendPasswordRequestValidator +public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher) : ISendAuthenticationMethodValidator { private readonly ISendPasswordHasher _sendPasswordHasher = sendPasswordHasher; @@ -21,7 +21,7 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher { SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, $"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required." } }; - public GrantValidationResult ValidateSendPassword(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId) + public Task ValidateRequestAsync(ExtensionGrantValidationContext context, ResourcePassword resourcePassword, Guid sendId) { var request = context.Request.Raw; var clientHashedPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword); @@ -30,13 +30,13 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher if (clientHashedPassword == null) { // Request is the wrong shape and doesn't contain a passwordHashB64 field. - return new GrantValidationResult( + return Task.FromResult(new GrantValidationResult( TokenRequestErrors.InvalidRequest, errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired], new Dictionary { { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired } - }); + })); } // _sendPasswordHasher.PasswordHashMatches checks for an empty string so no need to do it before we make the call. @@ -46,16 +46,16 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher if (!hashMatches) { // Request is the correct shape but the passwordHashB64 doesn't match, hash could be empty. - return new GrantValidationResult( + return Task.FromResult(new GrantValidationResult( TokenRequestErrors.InvalidGrant, errorDescription: _sendPasswordValidatorErrorDescriptions[SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch], new Dictionary { { SendAccessConstants.SendAccessError, SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch } - }); + })); } - return BuildSendPasswordSuccessResult(sendId); + return Task.FromResult(BuildSendPasswordSuccessResult(sendId)); } /// @@ -67,7 +67,7 @@ public class SendPasswordRequestValidator(ISendPasswordHasher sendPasswordHasher { var claims = new List { - new(Claims.SendId, sendId.ToString()), + new(Claims.SendAccessClaims.SendId, sendId.ToString()), new(Claims.Type, IdentityClientType.Send.ToString()) }; diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index d4f2ad8045..95c067d884 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.IdentityServer; using Bit.Core.Settings; +using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; using Bit.Identity.IdentityServer; using Bit.Identity.IdentityServer.ClientProviders; @@ -26,7 +27,8 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient, SendPasswordRequestValidator>(); + services.AddTransient, SendEmailOtpRequestValidator>(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs index 27a0bc1bbc..bf5322d916 100644 --- a/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs +++ b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs @@ -12,7 +12,7 @@ public class SendAccessClaimsPrincipalExtensionsTests { // Arrange var guid = Guid.NewGuid(); - var claims = new[] { new Claim(Claims.SendId, guid.ToString()) }; + var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, guid.ToString()) }; var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); // Act @@ -30,19 +30,19 @@ public class SendAccessClaimsPrincipalExtensionsTests // Act & Assert var ex = Assert.Throws(() => principal.GetSendId()); - Assert.Equal("Send ID claim not found.", ex.Message); + Assert.Equal("send_id claim not found.", ex.Message); } [Fact] public void GetSendId_ThrowsInvalidOperationException_WhenClaimValueIsInvalid() { // Arrange - var claims = new[] { new Claim(Claims.SendId, "not-a-guid") }; + var claims = new[] { new Claim(Claims.SendAccessClaims.SendId, "not-a-guid") }; var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); // Act & Assert var ex = Assert.Throws(() => principal.GetSendId()); - Assert.Equal("Invalid Send ID claim value.", ex.Message); + Assert.Equal("Invalid send_id claim value.", ex.Message); } [Fact] diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 849a5130a3..242bcc60f3 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -247,11 +247,18 @@ public class HandlebarsMailServiceTests } } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. [Fact] - public void ServiceExists() + public async Task SendSendEmailOtpEmailAsync_SendsEmail() { - Assert.NotNull(_sut); + // Arrange + var email = "test@example.com"; + var token = "aToken"; + var subject = string.Format("Your Bitwarden Send verification code is {0}", token); + + // Act + await _sut.SendSendEmailOtpEmailAsync(email, token, subject); + + // Assert + await _mailDeliveryService.Received(1).SendEmailAsync(Arg.Any()); } } diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs index 4b8c267861..3b0cf2c282 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs @@ -213,8 +213,8 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory services.AddSingleton(sendAuthQuery); // Mock password validator to return success - var passwordValidator = Substitute.For(); - passwordValidator.ValidateSendPassword( + var passwordValidator = Substitute.For>(); + passwordValidator.ValidateRequestAsync( Arg.Any(), Arg.Any(), Arg.Any()) diff --git a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs new file mode 100644 index 0000000000..9d9bc03ef5 --- /dev/null +++ b/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs @@ -0,0 +1,256 @@ +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Enums; +using Bit.Core.IdentityServer; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.IntegrationTestCommon.Factories; +using Duende.IdentityModel; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.IntegrationTest.RequestValidation; + +public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture +{ + private readonly IdentityApplicationFactory _factory; + + public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest() + { + // Arrange + var sendId = Guid.NewGuid(); + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new EmailOtp(["test@example.com"])); + services.AddSingleton(sendAuthQuery); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId); // No email + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + Assert.Contains("email is required", content); + } + + [Fact] + public async Task SendAccess_EmailOtpProtectedSend_EmailWithoutOtp_SendsOtpEmail() + { + // Arrange + var sendId = Guid.NewGuid(); + var email = "test@example.com"; + var generatedToken = "123456"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new EmailOtp([email])); + services.AddSingleton(sendAuthQuery); + + // Mock OTP token provider + var otpProvider = Substitute.For>(); + otpProvider.GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(generatedToken); + services.AddSingleton(otpProvider); + + // Mock mail service + var mailService = Substitute.For(); + services.AddSingleton(mailService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + Assert.Contains("email otp sent", content); + } + + [Fact] + public async Task SendAccess_EmailOtpProtectedSend_ValidOtp_ReturnsAccessToken() + { + // Arrange + var sendId = Guid.NewGuid(); + var email = "test@example.com"; + var otp = "123456"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new EmailOtp(new[] { email })); + services.AddSingleton(sendAuthQuery); + + // Mock OTP token provider to validate successfully + var otpProvider = Substitute.For>(); + otpProvider.ValidateTokenAsync(otp, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(true); + services.AddSingleton(otpProvider); + + var mailService = Substitute.For(); + services.AddSingleton(mailService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: otp); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + Assert.True(response.IsSuccessStatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenResponse.AccessToken, content); + Assert.Contains(OidcConstants.TokenResponse.BearerTokenType, content); + } + + [Fact] + public async Task SendAccess_EmailOtpProtectedSend_InvalidOtp_ReturnsInvalidGrant() + { + // Arrange + var sendId = Guid.NewGuid(); + var email = "test@example.com"; + var invalidOtp = "wrong123"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new EmailOtp(new[] { email })); + services.AddSingleton(sendAuthQuery); + + // Mock OTP token provider to validate as false + var otpProvider = Substitute.For>(); + otpProvider.ValidateTokenAsync(invalidOtp, Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(false); + services.AddSingleton(otpProvider); + + var mailService = Substitute.For(); + services.AddSingleton(mailService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, sendEmail: email, emailOtp: invalidOtp); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); + Assert.Contains("email otp is invalid", content); + } + + [Fact] + public async Task SendAccess_EmailOtpProtectedSend_OtpGenerationFails_ReturnsInvalidRequest() + { + // Arrange + var sendId = Guid.NewGuid(); + var email = "test@example.com"; + + var client = _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new EmailOtp(new[] { email })); + services.AddSingleton(sendAuthQuery); + + // Mock OTP token provider to fail generation + var otpProvider = Substitute.For>(); + otpProvider.GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns((string)null); + services.AddSingleton(otpProvider); + + var mailService = Substitute.For(); + services.AddSingleton(mailService); + }); + }).CreateClient(); + + var requestBody = CreateTokenRequestBody(sendId, sendEmail: email); // Email but no OTP + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + } + + private static FormUrlEncodedContent CreateTokenRequestBody(Guid sendId, + string sendEmail = null, string emailOtp = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + var parameters = new List> + { + new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), + new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ), + new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), + new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()), + new(SendAccessConstants.TokenRequest.SendId, sendIdBase64) + }; + + if (!string.IsNullOrEmpty(sendEmail)) + { + parameters.Add(new KeyValuePair( + SendAccessConstants.TokenRequest.Email, sendEmail)); + } + + if (!string.IsNullOrEmpty(emailOtp)) + { + parameters.Add(new KeyValuePair( + SendAccessConstants.TokenRequest.Otp, emailOtp)); + } + + return new FormUrlEncodedContent(parameters); + } +} diff --git a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs similarity index 90% rename from test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs rename to test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs index c3d422c51a..e651709c47 100644 --- a/test/Identity.Test/IdentityServer/SendAccessGrantValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs @@ -17,7 +17,7 @@ using Duende.IdentityServer.Validation; using NSubstitute; using Xunit; -namespace Bit.Identity.Test.IdentityServer; +namespace Bit.Identity.Test.IdentityServer.SendAccess; [SutProviderCustomize] public class SendAccessGrantValidatorTests @@ -167,7 +167,7 @@ public class SendAccessGrantValidatorTests // get the claims from the subject var claims = subject.Claims.ToList(); Assert.NotEmpty(claims); - Assert.Contains(claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString()); + Assert.Contains(claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString()); Assert.Contains(claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); } @@ -189,8 +189,8 @@ public class SendAccessGrantValidatorTests .GetAuthenticationMethod(sendId) .Returns(resourcePassword); - sutProvider.GetDependency() - .ValidateSendPassword(context, resourcePassword, sendId) + sutProvider.GetDependency>() + .ValidateRequestAsync(context, resourcePassword, sendId) .Returns(expectedResult); // Act @@ -198,15 +198,16 @@ public class SendAccessGrantValidatorTests // Assert Assert.Equal(expectedResult, context.Result); - sutProvider.GetDependency() + await sutProvider.GetDependency>() .Received(1) - .ValidateSendPassword(context, resourcePassword, sendId); + .ValidateRequestAsync(context, resourcePassword, sendId); } [Theory, BitAutoData] - public async Task ValidateAsync_EmailOtpMethod_NotImplemented_ThrowsError( + public async Task ValidateAsync_EmailOtpMethod_CallsEmailOtp( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, + GrantValidationResult expectedResult, Guid sendId, EmailOtp emailOtp) { @@ -216,15 +217,22 @@ public class SendAccessGrantValidatorTests sendId, tokenRequest); - sutProvider.GetDependency() .GetAuthenticationMethod(sendId) .Returns(emailOtp); + sutProvider.GetDependency>() + .ValidateRequestAsync(context, emailOtp, sendId) + .Returns(expectedResult); + // Act + await sutProvider.Sut.ValidateAsync(context); + // Assert - // Currently the EmailOtp case doesn't set a result, so it should be null - await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(context)); + Assert.Equal(expectedResult, context.Result); + await sutProvider.GetDependency>() + .Received(1) + .ValidateRequestAsync(context, emailOtp, sendId); } [Theory, BitAutoData] @@ -256,7 +264,7 @@ public class SendAccessGrantValidatorTests public void GrantType_ReturnsCorrectType() { // Arrange & Act - var validator = new SendAccessGrantValidator(null!, null!, null!); + var validator = new SendAccessGrantValidator(null!, null!, null!, null!); // Assert Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType); diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs new file mode 100644 index 0000000000..2fd21fd4cf --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs @@ -0,0 +1,310 @@ +using System.Collections.Specialized; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.IdentityServer; +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer.SendAccess; + +[SutProviderCustomize] +public class SendEmailOtpRequestValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateRequestAsync_MissingEmail_ReturnsInvalidRequest( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal("email is required.", result.ErrorDescription); + + // Verify no OTP generation or email sending occurred + await sutProvider.GetDependency>() + .DidNotReceive() + .GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendSendEmailOtpEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_EmailNotInList_ReturnsInvalidRequest( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + string email, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); + var emailOTP = new EmailOtp(["user@test.dev"]); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal("email is invalid.", result.ErrorDescription); + + // Verify no OTP generation or email sending occurred + await sutProvider.GetDependency>() + .DidNotReceive() + .GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()); + + await sutProvider.GetDependency() + .DidNotReceive() + .SendSendEmailOtpEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_EmailWithoutOtp_GeneratesAndSendsOtp( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + Guid sendId, + string email, + string generatedToken) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email); + + sutProvider.GetDependency>() + .GenerateTokenAsync( + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + expectedUniqueId) + .Returns(generatedToken); + + emailOtp = emailOtp with { Emails = [email] }; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal("email otp sent.", result.ErrorDescription); + + // Verify OTP generation + await sutProvider.GetDependency>() + .Received(1) + .GenerateTokenAsync( + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + expectedUniqueId); + + // Verify email sending + await sutProvider.GetDependency() + .Received(1) + .SendSendEmailOtpEmailAsync(email, generatedToken, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_OtpGenerationFails_ReturnsGenerationFailedError( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + Guid sendId, + string email) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + emailOtp = emailOtp with { Emails = [email] }; + + sutProvider.GetDependency>() + .GenerateTokenAsync(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns((string)null); // Generation fails + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + + // Verify no email was sent + await sutProvider.GetDependency() + .DidNotReceive() + .SendSendEmailOtpEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_ValidOtp_ReturnsSuccess( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + Guid sendId, + string email, + string otp) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + emailOtp = emailOtp with { Emails = [email] }; + + var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email); + + sutProvider.GetDependency>() + .ValidateTokenAsync( + otp, + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + expectedUniqueId) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.False(result.IsError); + var sub = result.Subject; + Assert.Equal(sendId.ToString(), sub.Claims.First(c => c.Type == Claims.SendAccessClaims.SendId).Value); + + // Verify claims + Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString()); + Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.Email && c.Value == email); + Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); + + // Verify OTP validation was called + await sutProvider.GetDependency>() + .Received(1) + .ValidateTokenAsync(otp, SendAccessConstants.OtpToken.TokenProviderName, SendAccessConstants.OtpToken.Purpose, expectedUniqueId); + + // Verify no email was sent (validation only) + await sutProvider.GetDependency() + .DidNotReceive() + .SendSendEmailOtpEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_InvalidOtp_ReturnsInvalidGrant( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + EmailOtp emailOtp, + Guid sendId, + string email, + string invalidOtp) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + emailOtp = emailOtp with { Emails = [email] }; + + var expectedUniqueId = string.Format(SendAccessConstants.OtpToken.TokenUniqueIdentifier, sendId, email); + + sutProvider.GetDependency>() + .ValidateTokenAsync(invalidOtp, + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + expectedUniqueId) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, emailOtp, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal("email otp is invalid.", result.ErrorDescription); + + // Verify OTP validation was attempted + await sutProvider.GetDependency>() + .Received(1) + .ValidateTokenAsync(invalidOtp, + SendAccessConstants.OtpToken.TokenProviderName, + SendAccessConstants.OtpToken.Purpose, + expectedUniqueId); + } + + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + var otpTokenProvider = Substitute.For>(); + var mailService = Substitute.For(); + + // Act + var validator = new SendEmailOtpRequestValidator(otpTokenProvider, mailService); + + // Assert + Assert.NotNull(validator); + } + + private static NameValueCollection CreateValidatedTokenRequest( + Guid sendId, + string sendEmail = null, + string otpCode = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + + var rawRequestParameters = new NameValueCollection + { + { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, + { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, + { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, + { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, + { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } + }; + + if (sendEmail != null) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail); + } + + if (otpCode != null && sendEmail != null) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode); + } + + return rawRequestParameters; + } +} diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs new file mode 100644 index 0000000000..e2b8b49830 --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs @@ -0,0 +1,297 @@ +using System.Collections.Specialized; +using Bit.Core.Auth.UserFeatures.SendAccess; +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.IdentityServer; +using Bit.Core.KeyManagement.Sends; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; +using Duende.IdentityServer.Validation; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer.SendAccess; + +[SutProviderCustomize] +public class SendPasswordRequestValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is required.", result.ErrorDescription); + + // Verify password hasher was not called + sutProvider.GetDependency() + .DidNotReceive() + .PasswordHashMatches(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal($"{SendAccessConstants.TokenRequest.ClientB64HashedPassword} is invalid.", result.ErrorDescription); + + // Verify password hasher was called with correct parameters + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_PasswordHashMatches_ReturnsSuccess( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.False(result.IsError); + + var sub = result.Subject; + Assert.Equal(sendId, sub.GetSendId()); + + // Verify claims + Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString()); + Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); + + // Verify password hasher was called + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, clientPasswordHash); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, string.Empty) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + + // Verify password hasher was called with empty string + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, string.Empty); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + var whitespacePassword = " "; + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, whitespacePassword) + .Returns(false); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + + // Verify password hasher was called with whitespace string + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, whitespacePassword); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId) + { + // Arrange + var firstPassword = "first-password"; + var secondPassword = "second-password"; + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(resourcePassword.Hash, firstPassword) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + + // Verify password hasher was called with first value + sutProvider.GetDependency() + .Received(1) + .PasswordHashMatches(resourcePassword.Hash, $"{firstPassword},{secondPassword}"); + } + + [Theory, BitAutoData] + public async Task ValidateSendPassword_SuccessResult_ContainsCorrectClaims( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + ResourcePassword resourcePassword, + Guid sendId, + string clientPasswordHash) + { + // Arrange + tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency() + .PasswordHashMatches(Arg.Any(), Arg.Any()) + .Returns(true); + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); + + // Assert + Assert.False(result.IsError); + var sub = result.Subject; + + var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendAccessClaims.SendId); + Assert.NotNull(sendIdClaim); + Assert.Equal(sendId.ToString(), sendIdClaim.Value); + + var typeClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.Type); + Assert.NotNull(typeClaim); + Assert.Equal(IdentityClientType.Send.ToString(), typeClaim.Value); + } + + [Fact] + public void Constructor_WithValidParameters_CreatesInstance() + { + // Arrange + var sendPasswordHasher = Substitute.For(); + + // Act + var validator = new SendPasswordRequestValidator(sendPasswordHasher); + + // Assert + Assert.NotNull(validator); + } + + private static NameValueCollection CreateValidatedTokenRequest( + Guid sendId, + params string[] passwordHash) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + + var rawRequestParameters = new NameValueCollection + { + { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, + { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, + { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, + { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, + { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } + }; + + if (passwordHash != null && passwordHash.Length > 0) + { + foreach (var hash in passwordHash) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash); + } + } + + return rawRequestParameters; + } +} diff --git a/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs index a776a70178..ccee33d8c7 100644 --- a/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs @@ -21,7 +21,7 @@ namespace Bit.Identity.Test.IdentityServer; public class SendPasswordRequestValidatorTests { [Theory, BitAutoData] - public void ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest( + public async Task ValidateSendPassword_MissingPasswordHash_ReturnsInvalidRequest( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -36,7 +36,7 @@ public class SendPasswordRequestValidatorTests }; // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.True(result.IsError); @@ -50,7 +50,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant( + public async Task ValidateSendPassword_PasswordHashMismatch_ReturnsInvalidGrant( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -70,7 +70,7 @@ public class SendPasswordRequestValidatorTests .Returns(false); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.True(result.IsError); @@ -84,7 +84,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_PasswordHashMatches_ReturnsSuccess( + public async Task ValidateSendPassword_PasswordHashMatches_ReturnsSuccess( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -104,7 +104,7 @@ public class SendPasswordRequestValidatorTests .Returns(true); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.False(result.IsError); @@ -113,7 +113,7 @@ public class SendPasswordRequestValidatorTests Assert.Equal(sendId, sub.GetSendId()); // Verify claims - Assert.Contains(sub.Claims, c => c.Type == Claims.SendId && c.Value == sendId.ToString()); + Assert.Contains(sub.Claims, c => c.Type == Claims.SendAccessClaims.SendId && c.Value == sendId.ToString()); Assert.Contains(sub.Claims, c => c.Type == Claims.Type && c.Value == IdentityClientType.Send.ToString()); // Verify password hasher was called @@ -123,7 +123,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher( + public async Task ValidateSendPassword_EmptyPasswordHash_CallsPasswordHasher( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -142,7 +142,7 @@ public class SendPasswordRequestValidatorTests .Returns(false); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.True(result.IsError); @@ -155,7 +155,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher( + public async Task ValidateSendPassword_WhitespacePasswordHash_CallsPasswordHasher( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -175,7 +175,7 @@ public class SendPasswordRequestValidatorTests .Returns(false); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.True(result.IsError); @@ -187,7 +187,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant( + public async Task ValidateSendPassword_MultiplePasswordHashParameters_ReturnsInvalidGrant( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -208,7 +208,7 @@ public class SendPasswordRequestValidatorTests .Returns(true); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.True(result.IsError); @@ -221,7 +221,7 @@ public class SendPasswordRequestValidatorTests } [Theory, BitAutoData] - public void ValidateSendPassword_SuccessResult_ContainsCorrectClaims( + public async Task ValidateSendPassword_SuccessResult_ContainsCorrectClaims( SutProvider sutProvider, [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, ResourcePassword resourcePassword, @@ -241,13 +241,13 @@ public class SendPasswordRequestValidatorTests .Returns(true); // Act - var result = sutProvider.Sut.ValidateSendPassword(context, resourcePassword, sendId); + var result = await sutProvider.Sut.ValidateRequestAsync(context, resourcePassword, sendId); // Assert Assert.False(result.IsError); var sub = result.Subject; - var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendId); + var sendIdClaim = sub.Claims.FirstOrDefault(c => c.Type == Claims.SendAccessClaims.SendId); Assert.NotNull(sendIdClaim); Assert.Equal(sendId.ToString(), sendIdClaim.Value); From 0bfbfaa17c36373b8cc16ea4a81c8979886b533f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 3 Sep 2025 11:38:01 +0200 Subject: [PATCH 185/326] Improve Swagger OperationIDs for Tools (#6239) --- .../Controllers/ImportCiphersController.cs | 2 +- src/Api/Tools/Controllers/SendsController.cs | 2 +- .../ImportCiphersControllerTests.cs | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Api/Tools/Controllers/ImportCiphersController.cs b/src/Api/Tools/Controllers/ImportCiphersController.cs index 0f29a9aee3..88028420b7 100644 --- a/src/Api/Tools/Controllers/ImportCiphersController.cs +++ b/src/Api/Tools/Controllers/ImportCiphersController.cs @@ -63,7 +63,7 @@ public class ImportCiphersController : Controller } [HttpPost("import-organization")] - public async Task PostImport([FromQuery] string organizationId, + public async Task PostImportOrganization([FromQuery] string organizationId, [FromBody] ImportOrganizationCiphersRequestModel model) { if (!_globalSettings.SelfHosted && diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 43239b3995..c02e9b0c20 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -192,7 +192,7 @@ public class SendsController : Controller } [HttpGet("")] - public async Task> Get() + public async Task> GetAll() { var userId = _userService.GetProperUserId(User).Value; var sends = await _sendRepository.GetManyByUserIdAsync(userId); diff --git a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs index 53d9d2a1f8..4908bb6847 100644 --- a/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/ImportCiphersControllerTests.cs @@ -126,7 +126,7 @@ public class ImportCiphersControllerTests }; // Act - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.PostImport(Arg.Any(), model)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.PostImportOrganization(Arg.Any(), model)); // Assert Assert.Equal("You cannot import this much data at once.", exception.Message); @@ -186,7 +186,7 @@ public class ImportCiphersControllerTests .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); // Act - await sutProvider.Sut.PostImport(orgId, request); + await sutProvider.Sut.PostImportOrganization(orgId, request); // Assert await sutProvider.GetDependency() @@ -257,7 +257,7 @@ public class ImportCiphersControllerTests .Returns(existingCollections.Select(c => new Collection { Id = orgIdGuid }).ToList()); // Act - await sutProvider.Sut.PostImport(orgId, request); + await sutProvider.Sut.PostImportOrganization(orgId, request); // Assert await sutProvider.GetDependency() @@ -324,7 +324,7 @@ public class ImportCiphersControllerTests // Act var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.PostImport(orgId, request)); + sutProvider.Sut.PostImportOrganization(orgId, request)); // Assert Assert.IsType(exception); @@ -387,7 +387,7 @@ public class ImportCiphersControllerTests // Act var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.PostImport(orgId, request)); + sutProvider.Sut.PostImportOrganization(orgId, request)); // Assert Assert.IsType(exception); @@ -457,7 +457,7 @@ public class ImportCiphersControllerTests // Act // User imports into collections and creates new collections // User has ImportCiphers and Create ciphers permission - await sutProvider.Sut.PostImport(orgId.ToString(), request); + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); // Assert await sutProvider.GetDependency() @@ -535,7 +535,7 @@ public class ImportCiphersControllerTests // User has ImportCiphers permission only and doesn't have Create permission var exception = await Assert.ThrowsAsync(async () => { - await sutProvider.Sut.PostImport(orgId.ToString(), request); + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); }); // Assert @@ -610,7 +610,7 @@ public class ImportCiphersControllerTests // Act // User imports/creates a new collection - existing collections not affected // User has create permissions and doesn't need import permissions - await sutProvider.Sut.PostImport(orgId.ToString(), request); + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); // Assert await sutProvider.GetDependency() @@ -685,7 +685,7 @@ public class ImportCiphersControllerTests // Act // User import into existing collection // User has ImportCiphers permission only and doesn't need create permission - await sutProvider.Sut.PostImport(orgId.ToString(), request); + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); // Assert await sutProvider.GetDependency() @@ -753,7 +753,7 @@ public class ImportCiphersControllerTests // import ciphers only and no collections // User has Create permissions // expected to be successful - await sutProvider.Sut.PostImport(orgId.ToString(), request); + await sutProvider.Sut.PostImportOrganization(orgId.ToString(), request); // Assert await sutProvider.GetDependency() From d627b0a0643650bb39132985c63ea0dfd4d253ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:01:39 +0200 Subject: [PATCH 186/326] [deps] Tools: Update aws-sdk-net monorepo (#6272) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 25e74d8aee..04dd7781bc 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -21,8 +21,8 @@ - - + + From 99058891d0ba39a7a6901799e81a6472b3556d88 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 3 Sep 2025 09:12:26 -0400 Subject: [PATCH 187/326] Auth/pm 24434/enhance email (#6157) * fix(emails): [PM-24434] Email Enhancement - Added seconds to new device logged in email --- src/Core/Services/Implementations/HandlebarsMailService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 394b5c5125..8de0e99bd3 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -559,7 +559,7 @@ public class HandlebarsMailService : IMailService SiteName = _globalSettings.SiteName, DeviceType = deviceType, TheDate = timestamp.ToLongDateString(), - TheTime = timestamp.ToShortTimeString(), + TheTime = timestamp.ToString("hh:mm:ss tt"), TimeZone = _utcTimeZoneDisplay, IpAddress = ip }; From 1dade9d4b868fb73907c0d280fd19bb0191692cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 3 Sep 2025 14:57:53 +0100 Subject: [PATCH 188/326] [PM-24233] Use BulkResourceCreationService in CipherRepository (#6201) * Add constant for CipherRepositoryBulkResourceCreation in FeatureFlagKeys * Add bulk creation methods for Ciphers, Folders, and CollectionCiphers in BulkResourceCreationService - Implemented CreateCiphersAsync, CreateFoldersAsync, CreateCollectionCiphersAsync, and CreateTempCiphersAsync methods for bulk insertion. - Added helper methods to build DataTables for Ciphers, Folders, and CollectionCiphers. - Enhanced error handling for empty collections during bulk operations. * Refactor CipherRepository to utilize BulkResourceCreationService - Introduced IFeatureService to manage feature flag checks for bulk operations. - Updated methods to conditionally use BulkResourceCreationService for creating Ciphers, Folders, and CollectionCiphers based on feature flag status. - Enhanced existing bulk copy logic to maintain functionality while integrating feature flag checks. * Add InlineFeatureService to DatabaseDataAttribute for feature flag management - Introduced EnabledFeatureFlags property to DatabaseDataAttribute for configuring feature flags. - Integrated InlineFeatureService to provide feature flag checks within the service collection. - Enhanced GetData method to utilize feature flags for conditional service registration. * Add tests for bulk creation of Ciphers in CipherRepositoryTests - Implemented tests for bulk creation of Ciphers, Folders, and Collections with feature flag checks. - Added test cases for updating multiple Ciphers to validate bulk update functionality. - Enhanced existing test structure to ensure comprehensive coverage of bulk operations in the CipherRepository. * Refactor BulkResourceCreationService to use dynamic types for DataColumns - Updated DataColumn definitions in BulkResourceCreationService to utilize the actual types of properties from the cipher object instead of hardcoded types. - Simplified the assignment of nullable properties to directly use their values, improving code readability and maintainability. * Update BulkResourceCreationService to use specific types for DataColumns - Changed DataColumn definitions to use specific types (short and string) instead of dynamic types based on cipher properties. - Improved handling of nullable properties when assigning values to DataTable rows, ensuring proper handling of DBNull for null values. * Refactor CipherRepositoryTests for improved clarity and consistency - Renamed test methods to better reflect their purpose and improve readability. - Updated test data to use more descriptive names for users, folders, and collections. - Enhanced test structure with clear Arrange, Act, and Assert sections for better understanding of test flow. - Ensured all tests validate the expected outcomes for bulk operations with feature flag checks. * Update CipherRepositoryBulkResourceCreation feature flag key * Refactor DatabaseDataAttribute usage in CipherRepositoryTests to use array syntax for EnabledFeatureFlags * Update CipherRepositoryTests to use GenerateComb for generating unique IDs * Refactor CipherRepository methods to accept a boolean parameter for enabling bulk resource creation based on feature flags. Update tests to verify functionality with and without the feature flag enabled. * Refactor CipherRepository and related services to support new methods for bulk resource creation without boolean parameters. --- src/Core/Constants.cs | 1 + .../RotateUserAccountkeysCommand.cs | 15 +- .../ImportFeatures/ImportCiphersCommand.cs | 20 +- .../Vault/Repositories/ICipherRepository.cs | 22 ++ .../Services/Implementations/CipherService.cs | 10 +- .../Helpers/BulkResourceCreationService.cs | 190 ++++++++++++++++ .../Vault/Repositories/CipherRepository.cs | 211 ++++++++++++++++++ .../Vault/Repositories/CipherRepository.cs | 41 ++++ .../ImportCiphersAsyncCommandTests.cs | 136 ++++++++++- .../Vault/Services/CipherServiceTests.cs | 53 +++++ .../Repositories/CipherRepositoryTests.cs | 157 +++++++++++++ 11 files changed, 849 insertions(+), 7 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 393ab15e4c..2993f6a094 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -114,6 +114,7 @@ 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 DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; + 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 6967c9bf85..011fc2932f 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -25,6 +25,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand private readonly IdentityErrorDescriber _identityErrorDescriber; private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IPasswordHasher _passwordHasher; + private readonly IFeatureService _featureService; /// /// Instantiates a new @@ -45,7 +46,8 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, IDeviceRepository deviceRepository, IPasswordHasher passwordHasher, - IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository) + IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository, + IFeatureService featureService) { _userService = userService; _userRepository = userRepository; @@ -59,6 +61,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand _identityErrorDescriber = errors; _credentialRepository = credentialRepository; _passwordHasher = passwordHasher; + _featureService = featureService; } /// @@ -100,7 +103,15 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand List saveEncryptedDataActions = new(); if (model.Ciphers.Any()) { - saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); + 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)); + } } if (model.Folders.Any()) diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index c7f7e3aff7..ce269bc68c 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -108,7 +108,15 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); + var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); + if (useBulkResourceCreationService) + { + await _cipherRepository.CreateAsync_vNext(importingUserId, ciphers, newFolders); + } + else + { + await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); + } // push await _pushService.PushSyncVaultAsync(importingUserId); @@ -183,7 +191,15 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - await _cipherRepository.CreateAsync(ciphers, newCollections, collectionCiphers, newCollectionUsers); + var useBulkResourceCreationService = _featureService.IsEnabled(FeatureFlagKeys.CipherRepositoryBulkResourceCreation); + if (useBulkResourceCreationService) + { + await _cipherRepository.CreateAsync_vNext(ciphers, newCollections, collectionCiphers, newCollectionUsers); + } + else + { + 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 5a04a6651d..60b6e21f1d 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -32,12 +32,28 @@ 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 RestoreAsync(IEnumerable ids, Guid userId); @@ -68,4 +84,10 @@ public interface ICipherRepository : IRepository /// A list of ciphers with updated data UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable ciphers); + /// + /// + /// 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 51ed4b0ce7..2a4cc6c137 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -642,7 +642,15 @@ public class CipherService : ICipherService cipherIds.Add(cipher.Id); } - await _cipherRepository.UpdateCiphersAsync(sharingUserId, cipherInfos.Select(c => c.cipher)); + 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 _collectionCipherRepository.UpdateCollectionsForCiphersAsync(cipherIds, sharingUserId, organizationId, collectionIds); diff --git a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs index 139960ceba..3610c1c484 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs @@ -1,5 +1,6 @@ using System.Data; using Bit.Core.Entities; +using Bit.Core.Vault.Entities; using Microsoft.Data.SqlClient; namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers; @@ -15,6 +16,38 @@ public static class BulkResourceCreationService await bulkCopy.WriteToServerAsync(dataTable); } + public static async Task CreateCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable ciphers, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "[dbo].[Cipher]"; + var dataTable = BuildCiphersTable(bulkCopy, ciphers, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + + public static async Task CreateFoldersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable folders, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "[dbo].[Folder]"; + var dataTable = BuildFoldersTable(bulkCopy, folders, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + + public static async Task CreateCollectionCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable collectionCiphers, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "[dbo].[CollectionCipher]"; + var dataTable = BuildCollectionCiphersTable(bulkCopy, collectionCiphers, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + + public static async Task CreateTempCiphersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable ciphers, string errorMessage = _defaultErrorMessage) + { + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); + bulkCopy.DestinationTableName = "#TempCipher"; + var dataTable = BuildCiphersTable(bulkCopy, ciphers, errorMessage); + await bulkCopy.WriteToServerAsync(dataTable); + } + private static DataTable BuildCollectionsUsersTable(SqlBulkCopy bulkCopy, IEnumerable collectionUsers, string errorMessage) { var collectionUser = collectionUsers.FirstOrDefault(); @@ -126,4 +159,161 @@ public static class BulkResourceCreationService return collectionsTable; } + + private static DataTable BuildCiphersTable(SqlBulkCopy bulkCopy, IEnumerable ciphers, string errorMessage) + { + var c = ciphers.FirstOrDefault(); + + if (c == null) + { + throw new ApplicationException(errorMessage); + } + + var ciphersTable = new DataTable("CipherDataTable"); + + var idColumn = new DataColumn(nameof(c.Id), c.Id.GetType()); + ciphersTable.Columns.Add(idColumn); + var userIdColumn = new DataColumn(nameof(c.UserId), typeof(Guid)); + ciphersTable.Columns.Add(userIdColumn); + var organizationId = new DataColumn(nameof(c.OrganizationId), typeof(Guid)); + ciphersTable.Columns.Add(organizationId); + var typeColumn = new DataColumn(nameof(c.Type), typeof(short)); + ciphersTable.Columns.Add(typeColumn); + var dataColumn = new DataColumn(nameof(c.Data), typeof(string)); + ciphersTable.Columns.Add(dataColumn); + var favoritesColumn = new DataColumn(nameof(c.Favorites), typeof(string)); + ciphersTable.Columns.Add(favoritesColumn); + var foldersColumn = new DataColumn(nameof(c.Folders), typeof(string)); + ciphersTable.Columns.Add(foldersColumn); + var attachmentsColumn = new DataColumn(nameof(c.Attachments), typeof(string)); + ciphersTable.Columns.Add(attachmentsColumn); + var creationDateColumn = new DataColumn(nameof(c.CreationDate), c.CreationDate.GetType()); + ciphersTable.Columns.Add(creationDateColumn); + var revisionDateColumn = new DataColumn(nameof(c.RevisionDate), c.RevisionDate.GetType()); + ciphersTable.Columns.Add(revisionDateColumn); + var deletedDateColumn = new DataColumn(nameof(c.DeletedDate), typeof(DateTime)); + ciphersTable.Columns.Add(deletedDateColumn); + var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short)); + ciphersTable.Columns.Add(repromptColumn); + var keyColummn = new DataColumn(nameof(c.Key), typeof(string)); + ciphersTable.Columns.Add(keyColummn); + + foreach (DataColumn col in ciphersTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + var keys = new DataColumn[1]; + keys[0] = idColumn; + ciphersTable.PrimaryKey = keys; + + foreach (var cipher in ciphers) + { + var row = ciphersTable.NewRow(); + + row[idColumn] = cipher.Id; + row[userIdColumn] = cipher.UserId.HasValue ? (object)cipher.UserId.Value : DBNull.Value; + row[organizationId] = cipher.OrganizationId.HasValue ? (object)cipher.OrganizationId.Value : DBNull.Value; + row[typeColumn] = (short)cipher.Type; + row[dataColumn] = cipher.Data; + row[favoritesColumn] = cipher.Favorites; + row[foldersColumn] = cipher.Folders; + row[attachmentsColumn] = cipher.Attachments; + row[creationDateColumn] = cipher.CreationDate; + row[revisionDateColumn] = cipher.RevisionDate; + row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value; + row[repromptColumn] = cipher.Reprompt.HasValue ? cipher.Reprompt.Value : DBNull.Value; + row[keyColummn] = cipher.Key; + + ciphersTable.Rows.Add(row); + } + + return ciphersTable; + } + + private static DataTable BuildFoldersTable(SqlBulkCopy bulkCopy, IEnumerable folders, string errorMessage) + { + var f = folders.FirstOrDefault(); + + if (f == null) + { + throw new ApplicationException(errorMessage); + } + + var foldersTable = new DataTable("FolderDataTable"); + + var idColumn = new DataColumn(nameof(f.Id), f.Id.GetType()); + foldersTable.Columns.Add(idColumn); + var userIdColumn = new DataColumn(nameof(f.UserId), f.UserId.GetType()); + foldersTable.Columns.Add(userIdColumn); + var nameColumn = new DataColumn(nameof(f.Name), typeof(string)); + foldersTable.Columns.Add(nameColumn); + var creationDateColumn = new DataColumn(nameof(f.CreationDate), f.CreationDate.GetType()); + foldersTable.Columns.Add(creationDateColumn); + var revisionDateColumn = new DataColumn(nameof(f.RevisionDate), f.RevisionDate.GetType()); + foldersTable.Columns.Add(revisionDateColumn); + + foreach (DataColumn col in foldersTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + var keys = new DataColumn[1]; + keys[0] = idColumn; + foldersTable.PrimaryKey = keys; + + foreach (var folder in folders) + { + var row = foldersTable.NewRow(); + + row[idColumn] = folder.Id; + row[userIdColumn] = folder.UserId; + row[nameColumn] = folder.Name; + row[creationDateColumn] = folder.CreationDate; + row[revisionDateColumn] = folder.RevisionDate; + + foldersTable.Rows.Add(row); + } + + return foldersTable; + } + + private static DataTable BuildCollectionCiphersTable(SqlBulkCopy bulkCopy, IEnumerable collectionCiphers, string errorMessage) + { + var cc = collectionCiphers.FirstOrDefault(); + + if (cc == null) + { + throw new ApplicationException(errorMessage); + } + + var collectionCiphersTable = new DataTable("CollectionCipherDataTable"); + + var collectionIdColumn = new DataColumn(nameof(cc.CollectionId), cc.CollectionId.GetType()); + collectionCiphersTable.Columns.Add(collectionIdColumn); + var cipherIdColumn = new DataColumn(nameof(cc.CipherId), cc.CipherId.GetType()); + collectionCiphersTable.Columns.Add(cipherIdColumn); + + foreach (DataColumn col in collectionCiphersTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + var keys = new DataColumn[2]; + keys[0] = collectionIdColumn; + keys[1] = cipherIdColumn; + collectionCiphersTable.PrimaryKey = keys; + + foreach (var collectionCipher in collectionCiphers) + { + var row = collectionCiphersTable.NewRow(); + + row[collectionIdColumn] = collectionCipher.CollectionId; + row[cipherIdColumn] = collectionCipher.CipherId; + + collectionCiphersTable.Rows.Add(row); + } + + return collectionCiphersTable; + } } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 180a90fd41..8c1f04affc 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -10,6 +10,7 @@ using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; 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; @@ -408,6 +409,52 @@ public class CipherRepository : Repository, ICipherRepository }; } + /// + 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); + + // 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 async Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers) { if (!ciphers.Any()) @@ -490,6 +537,83 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task UpdateCiphersAsync_vNext(Guid userId, IEnumerable ciphers) + { + if (!ciphers.Any()) + { + 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. + await BulkResourceCreationService.CreateTempCiphersAsync(connection, transaction, ciphers); + + // 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 CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders) { if (!ciphers.Any()) @@ -538,6 +662,44 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task CreateAsync_vNext(Guid userId, IEnumerable ciphers, IEnumerable folders) + { + if (!ciphers.Any()) + { + return; + } + + using (var connection = new SqlConnection(ConnectionString)) + { + connection.Open(); + + using (var transaction = connection.BeginTransaction()) + { + try + { + if (folders.Any()) + { + await BulkResourceCreationService.CreateFoldersAsync(connection, transaction, folders); + } + + await BulkResourceCreationService.CreateCiphersAsync(connection, transaction, ciphers); + + await connection.ExecuteAsync( + $"[{Schema}].[User_BumpAccountRevisionDate]", + new { Id = userId }, + commandType: CommandType.StoredProcedure, transaction: transaction); + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + } + } + public async Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers) { @@ -607,6 +769,55 @@ public class CipherRepository : Repository, ICipherRepository } } + 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(); + + using (var transaction = connection.BeginTransaction()) + { + try + { + await BulkResourceCreationService.CreateCiphersAsync(connection, transaction, ciphers); + + if (collections.Any()) + { + await BulkResourceCreationService.CreateCollectionsAsync(connection, transaction, collections); + } + + if (collectionCiphers.Any()) + { + await BulkResourceCreationService.CreateCollectionCiphersAsync(connection, transaction, collectionCiphers); + } + + if (collectionUsers.Any()) + { + await BulkResourceCreationService.CreateCollectionsUsersAsync(connection, transaction, collectionUsers); + } + + 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 SoftDeleteAsync(IEnumerable ids, Guid userId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 3fae537a1e..d595fe7cfe 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -167,6 +167,16 @@ 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, @@ -205,6 +215,18 @@ 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 ToggleCipherStates(ids, userId, CipherStateAction.HardDelete); @@ -907,6 +929,15 @@ 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()) @@ -970,6 +1001,16 @@ 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 0cb0deaf52..11f637d207 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -47,7 +47,41 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); // Assert - await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(importingUserId, ciphers, Arg.Any>()); + 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); } @@ -77,7 +111,45 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(importingUserId, ciphers, Arg.Any>()); + 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); } @@ -187,6 +259,66 @@ 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 55db5a9143..44c86389e3 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -674,6 +674,32 @@ 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) @@ -1094,6 +1120,33 @@ 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 0a186e43be..2a31398a02 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -8,11 +8,13 @@ using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Xunit; +using CipherType = Bit.Core.Vault.Enums.CipherType; namespace Bit.Infrastructure.IntegrationTest.Repositories; @@ -975,6 +977,161 @@ public class CipherRepositoryTests Assert.Equal("new_attachments", updatedCipher2.Attachments); } + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_vNext_WithFolders_Works( + IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository) + { + // Arrange + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var folder1 = new Folder { Id = CoreHelpers.GenerateComb(), UserId = user.Id, Name = "Test Folder 1" }; + var folder2 = new Folder { Id = CoreHelpers.GenerateComb(), UserId = user.Id, Name = "Test Folder 2" }; + var cipher1 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" }; + var cipher2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.SecureNote, UserId = user.Id, Data = "" }; + + // Act + await cipherRepository.CreateAsync_vNext( + userId: user.Id, + ciphers: [cipher1, cipher2], + folders: [folder1, folder2]); + + // Assert + var readCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id); + var readCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id); + Assert.NotNull(readCipher1); + Assert.NotNull(readCipher2); + + var readFolder1 = await folderRepository.GetByIdAsync(folder1.Id); + var readFolder2 = await folderRepository.GetByIdAsync(folder2.Id); + Assert.NotNull(readFolder1); + Assert.NotNull(readFolder2); + } + + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_vNext_WithCollectionsAndUsers_Works( + IOrganizationRepository orgRepository, + IOrganizationUserRepository orgUserRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository, + ICipherRepository cipherRepository, + IUserRepository userRepository) + { + // Arrange + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var org = await orgRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = user.Email, + Plan = "Test" + }); + + var orgUser = await orgUserRepository.CreateAsync(new OrganizationUser + { + UserId = user.Id, + OrganizationId = org.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + var collection = new Collection { Id = CoreHelpers.GenerateComb(), Name = "Test Collection", OrganizationId = org.Id }; + var cipher = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, OrganizationId = org.Id, Data = "" }; + var collectionCipher = new CollectionCipher { CollectionId = collection.Id, CipherId = cipher.Id }; + var collectionUser = new CollectionUser + { + CollectionId = collection.Id, + OrganizationUserId = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + }; + + // Act + await cipherRepository.CreateAsync_vNext( + ciphers: [cipher], + collections: [collection], + collectionCiphers: [collectionCipher], + collectionUsers: [collectionUser]); + + // Assert + var orgCiphers = await cipherRepository.GetManyByOrganizationIdAsync(org.Id); + Assert.Contains(orgCiphers, c => c.Id == cipher.Id); + + var collCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(org.Id); + Assert.Contains(collCiphers, cc => cc.CipherId == cipher.Id && cc.CollectionId == collection.Id); + + var collectionsInOrg = await collectionRepository.GetManyByOrganizationIdAsync(org.Id); + Assert.Contains(collectionsInOrg, c => c.Id == collection.Id); + + var collectionUsers = await collectionRepository.GetManyUsersByIdAsync(collection.Id); + var foundCollectionUser = collectionUsers.FirstOrDefault(cu => cu.Id == orgUser.Id); + Assert.NotNull(foundCollectionUser); + Assert.True(foundCollectionUser.Manage); + Assert.False(foundCollectionUser.ReadOnly); + Assert.False(foundCollectionUser.HidePasswords); + } + + [DatabaseTheory, DatabaseData] + public async Task UpdateCiphersAsync_vNext_Works( + IUserRepository userRepository, ICipherRepository cipherRepository) + { + // Arrange + var expectedNewType = CipherType.SecureNote; + var expectedNewAttachments = "bulk_new_attachments"; + + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var c1 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" }; + var c2 = new Cipher { Id = CoreHelpers.GenerateComb(), Type = CipherType.Login, UserId = user.Id, Data = "" }; + await cipherRepository.CreateAsync( + userId: user.Id, + ciphers: [c1, c2], + folders: []); + + c1.Type = expectedNewType; + c2.Attachments = expectedNewAttachments; + + // Act + await cipherRepository.UpdateCiphersAsync_vNext(user.Id, [c1, c2]); + + // Assert + var updated1 = await cipherRepository.GetByIdAsync(c1.Id); + Assert.NotNull(updated1); + Assert.Equal(c1.Id, updated1.Id); + Assert.Equal(expectedNewType, updated1.Type); + Assert.Equal(c1.UserId, updated1.UserId); + Assert.Equal(c1.Data, updated1.Data); + Assert.Equal(c1.OrganizationId, updated1.OrganizationId); + Assert.Equal(c1.Attachments, updated1.Attachments); + + var updated2 = await cipherRepository.GetByIdAsync(c2.Id); + Assert.NotNull(updated2); + Assert.Equal(c2.Id, updated2.Id); + Assert.Equal(c2.Type, updated2.Type); + Assert.Equal(c2.UserId, updated2.UserId); + Assert.Equal(c2.Data, updated2.Data); + Assert.Equal(c2.OrganizationId, updated2.OrganizationId); + Assert.Equal(expectedNewAttachments, updated2.Attachments); + } + [DatabaseTheory, DatabaseData] public async Task DeleteCipherWithSecurityTaskAsync_Works( IOrganizationRepository organizationRepository, From fa8d65cc1f572fad047e3da17eebb299fa097ef2 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:33:32 +0530 Subject: [PATCH 189/326] [PM 19727] Update InvoiceUpcoming email content (#6168) * changes to implement the email * Refactoring and fix the unit testing * refactor the code and remove used method * Fix the failing test * Update the email templates * remove the extra space here * Refactor the descriptions * Fix the wrong subject header * Add the in the hyperlink rather than just Help center --- .../Implementations/UpcomingInvoiceHandler.cs | 40 +- .../Billing/Extensions/InvoiceExtensions.cs | 76 ++++ .../Handlebars/Layouts/ProviderFull.html.hbs | 211 ++++++++++ .../ProviderInvoiceUpcoming.html.hbs | 89 ++++ .../ProviderInvoiceUpcoming.text.hbs | 41 ++ .../Models/Mail/InvoiceUpcomingViewModel.cs | 5 + src/Core/Services/IMailService.cs | 8 + .../Implementations/HandlebarsMailService.cs | 42 ++ .../NoopImplementations/NoopMailService.cs | 9 + .../Extensions/InvoiceExtensionsTests.cs | 394 ++++++++++++++++++ 10 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 src/Core/Billing/Extensions/InvoiceExtensions.cs create mode 100644 src/Core/MailTemplates/Handlebars/Layouts/ProviderFull.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.text.hbs create mode 100644 test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 9b1d110b5e..9f6fda7d3f 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; @@ -18,6 +19,7 @@ using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; public class UpcomingInvoiceHandler( + IGetPaymentMethodQuery getPaymentMethodQuery, ILogger logger, IMailService mailService, IOrganizationRepository organizationRepository, @@ -137,7 +139,7 @@ public class UpcomingInvoiceHandler( await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id); - await SendUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice); + await SendProviderUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice, subscription, providerId.Value); } } @@ -158,6 +160,42 @@ public class UpcomingInvoiceHandler( } } + private async Task SendProviderUpcomingInvoiceEmailsAsync(IEnumerable emails, Invoice invoice, Subscription subscription, Guid providerId) + { + var validEmails = emails.Where(e => !string.IsNullOrEmpty(e)); + + var items = invoice.FormatForProvider(subscription); + + if (invoice.NextPaymentAttempt.HasValue && invoice.AmountDue > 0) + { + var provider = await providerRepository.GetByIdAsync(providerId); + if (provider == null) + { + logger.LogWarning("Provider {ProviderId} not found for invoice upcoming email", providerId); + return; + } + + var collectionMethod = subscription.CollectionMethod; + var paymentMethod = await getPaymentMethodQuery.Run(provider); + + var hasPaymentMethod = paymentMethod != null; + var paymentMethodDescription = paymentMethod?.Match( + bankAccount => $"Bank account ending in {bankAccount.Last4}", + card => $"{card.Brand} ending in {card.Last4}", + payPal => $"PayPal account {payPal.Email}" + ); + + await mailService.SendProviderInvoiceUpcoming( + validEmails, + invoice.AmountDue / 100M, + invoice.NextPaymentAttempt.Value, + items, + collectionMethod, + hasPaymentMethod, + paymentMethodDescription); + } + } + private async Task AlignOrganizationTaxConcernsAsync( Organization organization, Subscription subscription, diff --git a/src/Core/Billing/Extensions/InvoiceExtensions.cs b/src/Core/Billing/Extensions/InvoiceExtensions.cs new file mode 100644 index 0000000000..bb9f7588bf --- /dev/null +++ b/src/Core/Billing/Extensions/InvoiceExtensions.cs @@ -0,0 +1,76 @@ +using System.Text.RegularExpressions; +using Stripe; + +namespace Bit.Core.Billing.Extensions; + +public static class InvoiceExtensions +{ + /// + /// Formats invoice line items specifically for provider invoices, standardizing product descriptions + /// and ensuring consistent tax representation. + /// + /// The Stripe invoice containing line items + /// The associated subscription (for future extensibility) + /// A list of formatted invoice item descriptions + public static List FormatForProvider(this Invoice invoice, Subscription subscription) + { + var items = new List(); + + // Return empty list if no line items + if (invoice.Lines == null) + { + return items; + } + + foreach (var line in invoice.Lines.Data ?? new List()) + { + // Skip null lines or lines without description + if (line?.Description == null) + { + continue; + } + + var description = line.Description; + + // Handle Provider Portal and Business Unit Portal service lines + if (description.Contains("Provider Portal") || description.Contains("Business Unit")) + { + var priceMatch = Regex.Match(description, @"\(at \$[\d,]+\.?\d* / month\)"); + var priceInfo = priceMatch.Success ? priceMatch.Value : ""; + + var standardizedDescription = $"{line.Quantity} × Manage service provider {priceInfo}"; + items.Add(standardizedDescription); + } + // Handle tax lines + else if (description.ToLower().Contains("tax")) + { + var priceMatch = Regex.Match(description, @"\(at \$[\d,]+\.?\d* / month\)"); + var priceInfo = priceMatch.Success ? priceMatch.Value : ""; + + // If no price info found in description, calculate from amount + if (string.IsNullOrEmpty(priceInfo) && line.Quantity > 0) + { + var pricePerItem = (line.Amount / 100m) / line.Quantity; + priceInfo = $"(at ${pricePerItem:F2} / month)"; + } + + var taxDescription = $"{line.Quantity} × Tax {priceInfo}"; + items.Add(taxDescription); + } + // Handle other line items as-is + else + { + items.Add(description); + } + } + + // Add fallback tax from invoice-level tax if present and not already included + if (invoice.Tax.HasValue && invoice.Tax.Value > 0) + { + var taxAmount = invoice.Tax.Value / 100m; + items.Add($"1 × Tax (at ${taxAmount:F2} / month)"); + } + + return items; + } +} diff --git a/src/Core/MailTemplates/Handlebars/Layouts/ProviderFull.html.hbs b/src/Core/MailTemplates/Handlebars/Layouts/ProviderFull.html.hbs new file mode 100644 index 0000000000..33e32c2bb0 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/Layouts/ProviderFull.html.hbs @@ -0,0 +1,211 @@ + + + + + + Bitwarden + + + + + {{! Yahoo center fix }} + + + + +
+ {{! 600px container }} + + + {{! Left column (center fix) }} + + {{! Right column (center fix) }} + +
+ + + + + +
+ Bitwarden +
+ + + + + + +
+ + {{>@partial-block}} + +
+ + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.html.hbs b/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.html.hbs new file mode 100644 index 0000000000..d9061d1ffe --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.html.hbs @@ -0,0 +1,89 @@ +{{#>ProviderFull}} + + + + + {{#unless (eq CollectionMethod "send_invoice")}} + + + + {{/unless}} + {{#if Items}} + {{#unless (eq CollectionMethod "send_invoice")}} + + + + {{/unless}} + {{/if}} + + + + {{#unless (eq CollectionMethod "send_invoice")}} + + + + + {{/unless}} + + + + {{#if (eq CollectionMethod "send_invoice")}} + + + + {{/if}} + {{#unless (eq CollectionMethod "send_invoice")}} + + + + {{/unless}} +
+ {{#if (eq CollectionMethod "send_invoice")}} +
Your subscription will renew soon
+
On {{date DueDate 'MMMM dd, yyyy'}} we'll send you an invoice with a summary of the charges including tax.
+ {{else}} +
Your subscription will renew on {{date DueDate 'MMMM dd, yyyy'}}
+ {{#if HasPaymentMethod}} +
To avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount:
+ {{else}} +
To avoid any interruption in service, please add a payment method that can be charged for the following amount:
+ {{/if}} + {{/if}} +
+ {{usd AmountDue}} +
+ Summary Of Charges
+
+ {{#each Items}} +
{{this}}
+ {{/each}} +
+ {{#if (eq CollectionMethod "send_invoice")}} +
To avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay.
+ {{else}} + + {{/if}} +
+ + + + +
+ Update payment method +
+
+ {{#if (eq CollectionMethod "send_invoice")}} + + + + +
+ Contact Bitwarden Support +
+ {{/if}} +
+ For assistance managing your subscription, please visit the Help Center or contact Bitwarden Customer Support. +
+ For assistance managing your subscription, please visit the Help Center or contact Bitwarden Customer Support. +
+{{/ProviderFull}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.text.hbs b/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.text.hbs new file mode 100644 index 0000000000..c666e287a5 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/ProviderInvoiceUpcoming.text.hbs @@ -0,0 +1,41 @@ +{{#>BasicTextLayout}} +{{#if (eq CollectionMethod "send_invoice")}} +Your subscription will renew soon + +On {{date DueDate 'MMMM dd, yyyy'}} we'll send you an invoice with a summary of the charges including tax. +{{else}} +Your subscription will renew on {{date DueDate 'MMMM dd, yyyy'}} + + {{#if HasPaymentMethod}} +To avoid any interruption in service, please ensure your {{PaymentMethodDescription}} can be charged for the following amount: + {{else}} +To avoid any interruption in service, please add a payment method that can be charged for the following amount: + {{/if}} + +{{usd AmountDue}} +{{/if}} +{{#if Items}} +{{#unless (eq CollectionMethod "send_invoice")}} + +Summary Of Charges +------------------ +{{#each Items}} +{{this}} +{{/each}} +{{/unless}} +{{/if}} + +{{#if (eq CollectionMethod "send_invoice")}} +To avoid any interruption in service for you or your clients, please pay the invoice by the due date, or contact Bitwarden Customer Support to sign up for auto-pay. + +Contact Bitwarden Support: {{{ContactUrl}}} + +For assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/). +{{else}} + +{{/if}} + +{{#unless (eq CollectionMethod "send_invoice")}} +For assistance managing your subscription, please visit the **Help center** (https://bitwarden.com/help/update-billing-info) or **contact Bitwarden Customer Support** (https://bitwarden.com/contact/). +{{/unless}} +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs index 50f8256b3d..b63213b811 100644 --- a/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs +++ b/src/Core/Models/Mail/InvoiceUpcomingViewModel.cs @@ -10,4 +10,9 @@ public class InvoiceUpcomingViewModel : BaseMailModel public List Items { get; set; } public bool MentionInvoices { get; set; } public string UpdateBillingInfoUrl { get; set; } = "https://bitwarden.com/help/update-billing-info/"; + public string CollectionMethod { get; set; } + public bool HasPaymentMethod { get; set; } + public string PaymentMethodDescription { get; set; } + public string HelpUrl { get; set; } = "https://bitwarden.com/help/"; + public string ContactUrl { get; set; } = "https://bitwarden.com/contact/"; } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index a38328dc9d..6e61c4f8dd 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -59,6 +59,14 @@ public interface IMailService DateTime dueDate, List items, bool mentionInvoices); + Task SendProviderInvoiceUpcoming( + IEnumerable emails, + decimal amount, + DateTime dueDate, + List items, + string? collectionMethod, + bool hasPaymentMethod, + string? paymentMethodDescription); Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices); Task SendAddedCreditAsync(string email, decimal amount); Task SendLicenseExpiredAsync(IEnumerable emails, string? organizationName = null); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 8de0e99bd3..0410bad19e 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -478,6 +478,33 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendProviderInvoiceUpcoming( + IEnumerable emails, + decimal amount, + DateTime dueDate, + List items, + string? collectionMethod = null, + bool hasPaymentMethod = true, + string? paymentMethodDescription = null) + { + var message = CreateDefaultMessage("Your upcoming Bitwarden invoice", emails); + var model = new InvoiceUpcomingViewModel + { + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + AmountDue = amount, + DueDate = dueDate, + Items = items, + MentionInvoices = false, + CollectionMethod = collectionMethod, + HasPaymentMethod = hasPaymentMethod, + PaymentMethodDescription = paymentMethodDescription + }; + await AddMessageContentAsync(message, "ProviderInvoiceUpcoming", model); + message.Category = "ProviderInvoiceUpcoming"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices) { var message = CreateDefaultMessage("Payment Failed", email); @@ -708,6 +735,8 @@ public class HandlebarsMailService : IMailService Handlebars.RegisterTemplate("SecurityTasksHtmlLayout", securityTasksHtmlLayoutSource); var securityTasksTextLayoutSource = await ReadSourceAsync("Layouts.SecurityTasks.text"); Handlebars.RegisterTemplate("SecurityTasksTextLayout", securityTasksTextLayoutSource); + var providerFullHtmlLayoutSource = await ReadSourceAsync("Layouts.ProviderFull.html"); + Handlebars.RegisterTemplate("ProviderFull", providerFullHtmlLayoutSource); Handlebars.RegisterHelper("date", (writer, context, parameters) => { @@ -863,6 +892,19 @@ public class HandlebarsMailService : IMailService writer.WriteSafeString(string.Empty); } }); + + // Equality comparison helper for conditional templates. + Handlebars.RegisterHelper("eq", (context, arguments) => + { + if (arguments.Length != 2) + { + return false; + } + + var value1 = arguments[0]?.ToString(); + var value2 = arguments[1]?.ToString(); + return string.Equals(value1, value2, StringComparison.OrdinalIgnoreCase); + }); } public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index bc73fb5398..7ec05bb1f9 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -137,6 +137,15 @@ public class NoopMailService : IMailService List items, bool mentionInvoices) => Task.FromResult(0); + public Task SendProviderInvoiceUpcoming( + IEnumerable emails, + decimal amount, + DateTime dueDate, + List items, + string? collectionMethod = null, + bool hasPaymentMethod = true, + string? paymentMethodDescription = null) => Task.FromResult(0); + public Task SendPaymentFailedAsync(string email, decimal amount, bool mentionInvoices) { return Task.FromResult(0); diff --git a/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs b/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs new file mode 100644 index 0000000000..a30e5e896c --- /dev/null +++ b/test/Core.Test/Billing/Extensions/InvoiceExtensionsTests.cs @@ -0,0 +1,394 @@ +using Bit.Core.Billing.Extensions; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Extensions; + +public class InvoiceExtensionsTests +{ + private static Invoice CreateInvoiceWithLines(params InvoiceLineItem[] lineItems) + { + return new Invoice + { + Lines = new StripeList + { + Data = lineItems?.ToList() ?? new List() + } + }; + } + + #region FormatForProvider Tests + + [Fact] + public void FormatForProvider_NullLines_ReturnsEmptyList() + { + // Arrange + var invoice = new Invoice + { + Lines = null + }; + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FormatForProvider_EmptyLines_ReturnsEmptyList() + { + // Arrange + var invoice = CreateInvoiceWithLines(); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FormatForProvider_NullLineItem_SkipsNullLine() + { + // Arrange + var invoice = CreateInvoiceWithLines(null); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FormatForProvider_LineWithNullDescription_SkipsLine() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem { Description = null, Quantity = 1, Amount = 1000 } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void FormatForProvider_ProviderPortalTeams_FormatsCorrectly() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Teams (at $6.00 / month)", + Quantity = 5, + Amount = 3000 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_ProviderPortalEnterprise_FormatsCorrectly() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Enterprise (at $4.00 / month)", + Quantity = 10, + Amount = 4000 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_ProviderPortalWithoutPriceInfo_FormatsWithoutPrice() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Teams", + Quantity = 3, + Amount = 1800 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("3 × Manage service provider ", result[0]); + } + + [Fact] + public void FormatForProvider_BusinessUnitPortalEnterprise_FormatsCorrectly() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Business Unit Portal - Enterprise (at $5.00 / month)", + Quantity = 8, + Amount = 4000 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("8 × Manage service provider (at $5.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_BusinessUnitPortalGeneric_FormatsCorrectly() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Business Unit Portal (at $3.00 / month)", + Quantity = 2, + Amount = 600 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("2 × Manage service provider (at $3.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_TaxLineWithPriceInfo_FormatsCorrectly() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Tax (at $2.00 / month)", + Quantity = 1, + Amount = 200 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("1 × Tax (at $2.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_TaxLineWithoutPriceInfo_CalculatesPrice() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Tax", + Quantity = 2, + Amount = 400 // $4.00 total, $2.00 per item + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("2 × Tax (at $2.00 / month)", result[0]); + } + + [Fact] + public void FormatForProvider_TaxLineWithZeroQuantity_DoesNotCalculatePrice() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Tax", + Quantity = 0, + Amount = 200 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("0 × Tax ", result[0]); + } + + [Fact] + public void FormatForProvider_OtherLineItem_ReturnsAsIs() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Some other service", + Quantity = 1, + Amount = 1000 + } + ); + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("Some other service", result[0]); + } + + [Fact] + public void FormatForProvider_InvoiceLevelTax_AddsToResult() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Teams", + Quantity = 1, + Amount = 600 + } + ); + invoice.Tax = 120; // $1.20 in cents + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Equal(2, result.Count); + Assert.Equal("1 × Manage service provider ", result[0]); + Assert.Equal("1 × Tax (at $1.20 / month)", result[1]); + } + + [Fact] + public void FormatForProvider_NoInvoiceLevelTax_DoesNotAddTax() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Teams", + Quantity = 1, + Amount = 600 + } + ); + invoice.Tax = null; + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("1 × Manage service provider ", result[0]); + } + + [Fact] + public void FormatForProvider_ZeroInvoiceLevelTax_DoesNotAddTax() + { + // Arrange + var invoice = CreateInvoiceWithLines( + new InvoiceLineItem + { + Description = "Provider Portal - Teams", + Quantity = 1, + Amount = 600 + } + ); + invoice.Tax = 0; + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Single(result); + Assert.Equal("1 × Manage service provider ", result[0]); + } + + [Fact] + public void FormatForProvider_ComplexScenario_HandlesAllLineTypes() + { + // Arrange + var lineItems = new StripeList(); + lineItems.Data = new List + { + new InvoiceLineItem + { + Description = "Provider Portal - Teams (at $6.00 / month)", Quantity = 5, Amount = 3000 + }, + new InvoiceLineItem + { + Description = "Provider Portal - Enterprise (at $4.00 / month)", Quantity = 10, Amount = 4000 + }, + new InvoiceLineItem { Description = "Tax", Quantity = 1, Amount = 800 }, + new InvoiceLineItem { Description = "Custom Service", Quantity = 2, Amount = 2000 } + }; + + var invoice = new Invoice + { + Lines = lineItems, + Tax = 200 // Additional $2.00 tax + }; + var subscription = new Subscription(); + + // Act + var result = invoice.FormatForProvider(subscription); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal("5 × Manage service provider (at $6.00 / month)", result[0]); + Assert.Equal("10 × Manage service provider (at $4.00 / month)", result[1]); + Assert.Equal("1 × Tax (at $8.00 / month)", result[2]); + Assert.Equal("Custom Service", result[3]); + Assert.Equal("1 × Tax (at $2.00 / month)", result[4]); + } + + #endregion +} From ef8c7f656d87a7b0e3307489f92c571719d01012 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:03:49 -0500 Subject: [PATCH 190/326] [PM-24350] fix tax calculation (#6251) --- .../Services/ProviderBillingService.cs | 5 +- .../OrganizationCreateRequestModel.cs | 3 +- .../Request/Accounts/PremiumRequestModel.cs | 3 +- .../Accounts/TaxInfoUpdateRequestModel.cs | 3 +- .../Implementations/StripeEventService.cs | 2 +- .../Implementations/UpcomingInvoiceHandler.cs | 4 +- .../Billing/Extensions/BillingExtensions.cs | 13 + .../Extensions/ServiceCollectionExtensions.cs | 3 - .../Services/OrganizationBillingService.cs | 6 +- .../Commands/UpdateBillingAddressCommand.cs | 2 +- .../Implementations/SubscriberService.cs | 10 +- .../Tax/Commands/PreviewTaxAmountCommand.cs | 14 +- .../Tax/Services/IAutomaticTaxFactory.cs | 11 - .../Tax/Services/IAutomaticTaxStrategy.cs | 33 -- .../Implementations/AutomaticTaxFactory.cs | 50 -- .../BusinessUseAutomaticTaxStrategy.cs | 96 ---- .../PersonalUseAutomaticTaxStrategy.cs | 64 --- src/Core/Constants.cs | 13 + src/Core/Models/Business/TaxInfo.cs | 2 +- .../Implementations/StripePaymentService.cs | 24 +- .../Commands/PreviewTaxAmountCommandTests.cs | 267 +++++++++- .../Tax/Services/AutomaticTaxFactoryTests.cs | 105 ---- .../BusinessUseAutomaticTaxStrategyTests.cs | 492 ------------------ .../Tax/Services/FakeAutomaticTaxStrategy.cs | 35 -- .../PersonalUseAutomaticTaxStrategyTests.cs | 217 -------- .../Services/StripePaymentServiceTests.cs | 358 ++++++++++++- 26 files changed, 663 insertions(+), 1172 deletions(-) delete mode 100644 src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs delete mode 100644 src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs delete mode 100644 src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs delete mode 100644 src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs delete mode 100644 src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs delete mode 100644 test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs delete mode 100644 test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs delete mode 100644 test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs delete mode 100644 test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs 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 8c0b2c8275..5169d6cfd1 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -3,6 +3,7 @@ using System.Globalization; using Bit.Commercial.Core.Billing.Providers.Models; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -282,7 +283,7 @@ public class ProviderBillingService( ] }; - if (providerCustomer.Address is not { Country: "US" }) + if (providerCustomer.Address is not { Country: Constants.CountryAbbreviations.UnitedStates }) { customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; } @@ -525,7 +526,7 @@ public class ProviderBillingService( } }; - if (taxInfo.BillingAddressCountry is not "US") + if (taxInfo.BillingAddressCountry is not Constants.CountryAbbreviations.UnitedStates) { options.TaxExempt = StripeConstants.TaxExempt.Reverse; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 10f938adfe..7754c44c8c 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Bit.Core; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -139,7 +140,7 @@ public class OrganizationCreateRequestModel : IValidatableObject new string[] { nameof(BillingAddressCountry) }); } - if (PlanType != PlanType.Free && BillingAddressCountry == "US" && + if (PlanType != PlanType.Free && BillingAddressCountry == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(BillingAddressPostalCode)) { yield return new ValidationResult("Zip / postal code is required.", diff --git a/src/Api/Models/Request/Accounts/PremiumRequestModel.cs b/src/Api/Models/Request/Accounts/PremiumRequestModel.cs index 4e9882d67c..8e9aac8cc2 100644 --- a/src/Api/Models/Request/Accounts/PremiumRequestModel.cs +++ b/src/Api/Models/Request/Accounts/PremiumRequestModel.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core; using Bit.Core.Settings; using Enums = Bit.Core.Enums; @@ -35,7 +36,7 @@ public class PremiumRequestModel : IValidatableObject { yield return new ValidationResult("Payment token or license is required."); } - if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode)) + if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode)) { yield return new ValidationResult("Zip / postal code is required.", new string[] { nameof(PostalCode) }); diff --git a/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs b/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs index 5f58453a6d..d3e3f5ec55 100644 --- a/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs +++ b/src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core; namespace Bit.Api.Models.Request.Accounts; @@ -13,7 +14,7 @@ public class TaxInfoUpdateRequestModel : IValidatableObject public virtual IEnumerable Validate(ValidationContext validationContext) { - if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode)) + if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode)) { yield return new ValidationResult("Zip / postal code is required.", new string[] { nameof(PostalCode) }); diff --git a/src/Billing/Services/Implementations/StripeEventService.cs b/src/Billing/Services/Implementations/StripeEventService.cs index 7e2984e423..7eef357e14 100644 --- a/src/Billing/Services/Implementations/StripeEventService.cs +++ b/src/Billing/Services/Implementations/StripeEventService.cs @@ -218,7 +218,7 @@ public class StripeEventService : IStripeEventService private static string GetCustomerRegion(IDictionary customerMetadata) { - const string defaultRegion = "US"; + const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates; if (customerMetadata is null) { diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 9f6fda7d3f..e5675f7c0a 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -203,7 +203,7 @@ public class UpcomingInvoiceHandler( { var nonUSBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families && - subscription.Customer.Address.Country != "US"; + subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates; if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { @@ -248,7 +248,7 @@ public class UpcomingInvoiceHandler( Subscription subscription, string eventId) { - if (subscription.Customer.Address.Country != "US" && + if (subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { try diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index 55db9dde18..7f81bfd33f 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -22,6 +22,19 @@ public static class BillingExtensions _ => throw new BillingException($"PlanType {planType} could not be matched to a ProductTierType") }; + public static bool IsBusinessProductTierType(this PlanType planType) + => IsBusinessProductTierType(planType.GetProductTier()); + + public static bool IsBusinessProductTierType(this ProductTierType productTierType) + => productTierType switch + { + ProductTierType.Free => false, + ProductTierType.Families => false, + ProductTierType.Enterprise => true, + ProductTierType.Teams => true, + ProductTierType.TeamsStarter => true + }; + public static bool IsBillable(this Provider provider) => provider is { diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 39ee3ec1ec..147e96105a 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -25,9 +25,6 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddKeyedTransient(AutomaticTaxFactory.PersonalUse); - services.AddKeyedTransient(AutomaticTaxFactory.BusinessUse); - services.AddTransient(); services.AddLicenseServices(); services.AddPricingClient(); services.AddTransient(); diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 0e42803aaf..446f9563f9 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -275,7 +275,7 @@ public class OrganizationBillingService( if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && - customerSetup.TaxInformation.Country != "US") + customerSetup.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates) { customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; } @@ -514,14 +514,14 @@ public class OrganizationBillingService( customer = customer switch { - { Address.Country: not "US", TaxExempt: not StripeConstants.TaxExempt.Reverse } => await + { Address.Country: not Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: not StripeConstants.TaxExempt.Reverse } => await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { Expand = expansions, TaxExempt = StripeConstants.TaxExempt.Reverse }), - { Address.Country: "US", TaxExempt: StripeConstants.TaxExempt.Reverse } => await + { Address.Country: Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: StripeConstants.TaxExempt.Reverse } => await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { diff --git a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs index fdf519523a..f4eca40cae 100644 --- a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs +++ b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs @@ -84,7 +84,7 @@ public class UpdateBillingAddressCommand( State = billingAddress.State }, Expand = ["subscriptions", "tax_ids"], - TaxExempt = billingAddress.Country != "US" + TaxExempt = billingAddress.Country != Core.Constants.CountryAbbreviations.UnitedStates ? StripeConstants.TaxExempt.Reverse : StripeConstants.TaxExempt.None }); diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 63a9352020..84d41f829c 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -801,15 +801,13 @@ public class SubscriberService( _ => false }; - - if (isBusinessUseSubscriber) { switch (customer) { case { - Address.Country: not "US", + Address.Country: not Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: not TaxExempt.Reverse }: await stripeAdapter.CustomerUpdateAsync(customer.Id, @@ -817,7 +815,7 @@ public class SubscriberService( break; case { - Address.Country: "US", + Address.Country: Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: TaxExempt.Reverse }: await stripeAdapter.CustomerUpdateAsync(customer.Id, @@ -840,8 +838,8 @@ public class SubscriberService( { User => true, Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families || - customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), - Provider => customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), + customer.Address.Country == Core.Constants.CountryAbbreviations.UnitedStates || (customer.TaxIds?.Any() ?? false), + Provider => customer.Address.Country == Core.Constants.CountryAbbreviations.UnitedStates || (customer.TaxIds?.Any() ?? false), _ => false }; diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs index 6e061293c7..94d3724d73 100644 --- a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs +++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs @@ -95,17 +95,11 @@ public class PreviewTaxAmountCommand( } } - if (planType.GetProductTier() == ProductTierType.Families) + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + if (parameters.PlanType.IsBusinessProductTierType() && + parameters.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates) { - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - } - else - { - options.AutomaticTax = new InvoiceAutomaticTaxOptions - { - Enabled = options.CustomerDetails.Address.Country == "US" || - options.CustomerDetails.TaxIds is [_, ..] - }; + options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse; } var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); diff --git a/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs deleted file mode 100644 index c0a31efb3c..0000000000 --- a/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Bit.Core.Billing.Tax.Models; - -namespace Bit.Core.Billing.Tax.Services; - -/// -/// Responsible for defining the correct automatic tax strategy for either personal use of business use. -/// -public interface IAutomaticTaxFactory -{ - Task CreateAsync(AutomaticTaxFactoryParameters parameters); -} diff --git a/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs deleted file mode 100644 index 557bb1d30c..0000000000 --- a/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs +++ /dev/null @@ -1,33 +0,0 @@ -#nullable enable -using Stripe; - -namespace Bit.Core.Billing.Tax.Services; - -public interface IAutomaticTaxStrategy -{ - /// - /// - /// - /// - /// - /// Returns if changes are to be applied to the subscription, returns null - /// otherwise. - /// - SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription); - - /// - /// Modifies an existing object with the automatic tax flag set correctly. - /// - /// - /// - void SetCreateOptions(SubscriptionCreateOptions options, Customer customer); - - /// - /// Modifies an existing object with the automatic tax flag set correctly. - /// - /// - /// - void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription); - - void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options); -} diff --git a/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs deleted file mode 100644 index 6086a16b79..0000000000 --- a/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -#nullable enable -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Models; -using Bit.Core.Entities; -using Bit.Core.Services; - -namespace Bit.Core.Billing.Tax.Services.Implementations; - -public class AutomaticTaxFactory( - IFeatureService featureService, - IPricingClient pricingClient) : IAutomaticTaxFactory -{ - public const string BusinessUse = "business-use"; - public const string PersonalUse = "personal-use"; - - private readonly Lazy>> _personalUsePlansTask = new(async () => - { - var plans = await Task.WhenAll( - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)); - - return plans.Select(plan => plan.PasswordManager.StripePlanId); - }); - - public async Task CreateAsync(AutomaticTaxFactoryParameters parameters) - { - if (parameters.Subscriber is User) - { - return new PersonalUseAutomaticTaxStrategy(featureService); - } - - if (parameters.PlanType.HasValue) - { - var plan = await pricingClient.GetPlanOrThrow(parameters.PlanType.Value); - return plan.CanBeUsedByBusiness - ? new BusinessUseAutomaticTaxStrategy(featureService) - : new PersonalUseAutomaticTaxStrategy(featureService); - } - - var personalUsePlans = await _personalUsePlansTask.Value; - - if (parameters.Prices != null && parameters.Prices.Any(x => personalUsePlans.Any(y => y == x))) - { - return new PersonalUseAutomaticTaxStrategy(featureService); - } - - return new BusinessUseAutomaticTaxStrategy(featureService); - } -} diff --git a/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs deleted file mode 100644 index 6affc57354..0000000000 --- a/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs +++ /dev/null @@ -1,96 +0,0 @@ -#nullable enable -using Bit.Core.Billing.Extensions; -using Bit.Core.Services; -using Stripe; - -namespace Bit.Core.Billing.Tax.Services.Implementations; - -public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy -{ - public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription) - { - if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - { - return null; - } - - var shouldBeEnabled = ShouldBeEnabled(subscription.Customer); - if (subscription.AutomaticTax.Enabled == shouldBeEnabled) - { - return null; - } - - var options = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = shouldBeEnabled - }, - DefaultTaxRates = [] - }; - - return options; - } - - public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer) - { - options.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = ShouldBeEnabled(customer) - }; - } - - public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription) - { - if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - { - return; - } - - var shouldBeEnabled = ShouldBeEnabled(subscription.Customer); - - if (subscription.AutomaticTax.Enabled == shouldBeEnabled) - { - return; - } - - options.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = shouldBeEnabled - }; - options.DefaultTaxRates = []; - } - - public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options) - { - options.AutomaticTax ??= new InvoiceAutomaticTaxOptions(); - - if (options.CustomerDetails.Address.Country == "US") - { - options.AutomaticTax.Enabled = true; - return; - } - - options.AutomaticTax.Enabled = options.CustomerDetails.TaxIds != null && options.CustomerDetails.TaxIds.Any(); - } - - private bool ShouldBeEnabled(Customer customer) - { - if (!customer.HasRecognizedTaxLocation()) - { - return false; - } - - if (customer.Address.Country == "US") - { - return true; - } - - if (customer.TaxIds == null) - { - throw new ArgumentNullException(nameof(customer.TaxIds), "`customer.tax_ids` must be expanded."); - } - - return customer.TaxIds.Any(); - } -} diff --git a/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs deleted file mode 100644 index 615222259e..0000000000 --- a/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs +++ /dev/null @@ -1,64 +0,0 @@ -#nullable enable -using Bit.Core.Billing.Extensions; -using Bit.Core.Services; -using Stripe; - -namespace Bit.Core.Billing.Tax.Services.Implementations; - -public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy -{ - public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer) - { - options.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = ShouldBeEnabled(customer) - }; - } - - public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription) - { - if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - { - return; - } - options.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = ShouldBeEnabled(subscription.Customer) - }; - options.DefaultTaxRates = []; - } - - public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription) - { - if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - { - return null; - } - - if (subscription.AutomaticTax.Enabled == ShouldBeEnabled(subscription.Customer)) - { - return null; - } - - var options = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = ShouldBeEnabled(subscription.Customer), - }, - DefaultTaxRates = [] - }; - - return options; - } - - public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options) - { - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - } - - private static bool ShouldBeEnabled(Customer customer) - { - return customer.HasRecognizedTaxLocation(); - } -} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 2993f6a094..9ddbf5c600 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -52,6 +52,19 @@ public static class Constants /// regardless of whether there is a proration or not. ///
public const string AlwaysInvoice = "always_invoice"; + + /// + /// Used primarily to determine whether a customer's business is inside or outside the United States + /// for billing purposes. + /// + public static class CountryAbbreviations + { + /// + /// Abbreviation for The United States. + /// This value must match what Stripe uses for the `Country` field value for the United States. + /// + public const string UnitedStates = "US"; + } } public static class AuthConstants diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index 4daa9a268a..4f95bb393d 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -13,5 +13,5 @@ public class TaxInfo public string BillingAddressCity { get; set; } public string BillingAddressState { get; set; } public string BillingAddressPostalCode { get; set; } - public string BillingAddressCountry { get; set; } = "US"; + public string BillingAddressCountry { get; set; } = Constants.CountryAbbreviations.UnitedStates; } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 440fb5c546..ec45944bd2 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -9,11 +9,9 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Responses; using Bit.Core.Billing.Tax.Services; -using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -21,7 +19,6 @@ using Bit.Core.Models.BitStripe; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Settings; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; using PaymentMethod = Stripe.PaymentMethod; @@ -41,8 +38,6 @@ public class StripePaymentService : IPaymentService private readonly IFeatureService _featureService; private readonly ITaxService _taxService; private readonly IPricingClient _pricingClient; - private readonly IAutomaticTaxFactory _automaticTaxFactory; - private readonly IAutomaticTaxStrategy _personalUseTaxStrategy; public StripePaymentService( ITransactionRepository transactionRepository, @@ -52,9 +47,7 @@ public class StripePaymentService : IPaymentService IGlobalSettings globalSettings, IFeatureService featureService, ITaxService taxService, - IPricingClient pricingClient, - IAutomaticTaxFactory automaticTaxFactory, - [FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy) + IPricingClient pricingClient) { _transactionRepository = transactionRepository; _logger = logger; @@ -64,8 +57,6 @@ public class StripePaymentService : IPaymentService _featureService = featureService; _taxService = taxService; _pricingClient = pricingClient; - _automaticTaxFactory = automaticTaxFactory; - _personalUseTaxStrategy = personalUseTaxStrategy; } private async Task ChangeOrganizationSponsorship( @@ -137,7 +128,7 @@ public class StripePaymentService : IPaymentService { if (sub.Customer is { - Address.Country: not "US", + Address.Country: not Constants.CountryAbbreviations.UnitedStates, TaxExempt: not StripeConstants.TaxExempt.Reverse }) { @@ -987,8 +978,6 @@ public class StripePaymentService : IPaymentService } } - _personalUseTaxStrategy.SetInvoiceCreatePreviewOptions(options); - try { var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); @@ -1152,9 +1141,12 @@ public class StripePaymentService : IPaymentService } } - var automaticTaxFactoryParameters = new AutomaticTaxFactoryParameters(parameters.PasswordManager.Plan); - var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxFactoryParameters); - automaticTaxStrategy.SetInvoiceCreatePreviewOptions(options); + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + if (parameters.PasswordManager.Plan.IsBusinessProductTierType() && + parameters.TaxInformation.Country != Constants.CountryAbbreviations.UnitedStates) + { + options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse; + } try { diff --git a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs index ee5625d522..1de180cea1 100644 --- a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs +++ b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs @@ -181,7 +181,7 @@ public class PreviewTaxAmountCommandTests options.SubscriptionDetails.Items.Count == 1 && options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && options.SubscriptionDetails.Items[0].Quantity == 1 && - options.AutomaticTax.Enabled == false + options.AutomaticTax.Enabled == true )) .Returns(expectedInvoice); @@ -273,4 +273,269 @@ public class PreviewTaxAmountCommandTests var badRequest = result.AsT1; Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response); } + + [Fact] + public async Task Run_USBased_PersonalUse_SetsAutomaticTaxEnabled() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_USBased_BusinessUse_SetsAutomaticTaxEnabled() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_USBased_PersonalUse_DoesNotSetTaxExempt() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_USBased_BusinessUse_DoesNotSetTaxExempt() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + Assert.True(result.IsT0); + } + + [Fact] + public async Task Run_NonUSBased_PersonalUse_DoesNotSetTaxExempt() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + Assert.True(result.IsT0); + + } + + [Fact] + public async Task Run_NonUSBased_BusinessUse_SetsTaxExemptReverse() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse + )); + Assert.True(result.IsT0); + } } diff --git a/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs deleted file mode 100644 index d9d2679bca..0000000000 --- a/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.StaticStore.Plans; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Models; -using Bit.Core.Billing.Tax.Services.Implementations; -using Bit.Core.Entities; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Billing.Tax.Services; - -[SutProviderCustomize] -public class AutomaticTaxFactoryTests -{ - [BitAutoData] - [Theory] - public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsUser(SutProvider sut) - { - var parameters = new AutomaticTaxFactoryParameters(new User(), []); - - var actual = await sut.Sut.CreateAsync(parameters); - - Assert.IsType(actual); - } - - [BitAutoData] - [Theory] - public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsOrganizationWithFamiliesAnnuallyPrice( - SutProvider sut) - { - var familiesPlan = new FamiliesPlan(); - var parameters = new AutomaticTaxFactoryParameters(new Organization(), [familiesPlan.PasswordManager.StripePlanId]); - - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(new FamiliesPlan()); - - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually2019)) - .Returns(new Families2019Plan()); - - var actual = await sut.Sut.CreateAsync(parameters); - - Assert.IsType(actual); - } - - [Theory] - [BitAutoData] - public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenSubscriberIsOrganizationWithBusinessUsePrice( - EnterpriseAnnually plan, - SutProvider sut) - { - var parameters = new AutomaticTaxFactoryParameters(new Organization(), [plan.PasswordManager.StripePlanId]); - - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(new FamiliesPlan()); - - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually2019)) - .Returns(new Families2019Plan()); - - var actual = await sut.Sut.CreateAsync(parameters); - - Assert.IsType(actual); - } - - [Theory] - [BitAutoData] - public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenPlanIsMeantForPersonalUse(SutProvider sut) - { - var parameters = new AutomaticTaxFactoryParameters(PlanType.FamiliesAnnually); - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == parameters.PlanType.Value)) - .Returns(new FamiliesPlan()); - - var actual = await sut.Sut.CreateAsync(parameters); - - Assert.IsType(actual); - } - - [Theory] - [BitAutoData] - public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenPlanIsMeantForBusinessUse(SutProvider sut) - { - var parameters = new AutomaticTaxFactoryParameters(PlanType.EnterpriseAnnually); - sut.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == parameters.PlanType.Value)) - .Returns(new EnterprisePlan(true)); - - var actual = await sut.Sut.CreateAsync(parameters); - - Assert.IsType(actual); - } - - public record EnterpriseAnnually : EnterprisePlan - { - public EnterpriseAnnually() : base(true) - { - } - } -} diff --git a/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs deleted file mode 100644 index dc10d222f1..0000000000 --- a/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs +++ /dev/null @@ -1,492 +0,0 @@ -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Tax.Services.Implementations; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Stripe; -using Xunit; - -namespace Bit.Core.Test.Billing.Tax.Services; - -[SutProviderCustomize] -public class BusinessUseAutomaticTaxStrategyTests -{ - [Theory] - [BitAutoData] - public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled( - SutProvider sutProvider) - { - var subscription = new Subscription(); - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(false); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.Null(actual); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "US", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.Null(actual); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.False(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = "US", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.True(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List - { - new() - { - Country = "ES", - Type = "eu_vat", - Value = "ESZ8880999Z" - } - } - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.True(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = null - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - Assert.Throws(() => sutProvider.Sut.GetUpdateOptions(subscription)); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds( - SutProvider sutProvider) - { - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List() - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.False(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsNothing_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - Customer = new Customer - { - Address = new() - { - Country = "US" - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(false); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.Null(options.AutomaticTax); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsNothing_WhenSubscriptionDoesNotNeedUpdating( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "US", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.Null(options.AutomaticTax); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.False(options.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = "US", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.True(options.AutomaticTax!.Enabled); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List - { - new() - { - Country = "ES", - Type = "eu_vat", - Value = "ESZ8880999Z" - } - } - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.True(options.AutomaticTax!.Enabled); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = null - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - Assert.Throws(() => sutProvider.Sut.SetUpdateOptions(options, subscription)); - } - - [Theory] - [BitAutoData] - public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds( - SutProvider sutProvider) - { - var options = new SubscriptionUpdateOptions(); - - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "ES", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List() - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - sutProvider.Sut.SetUpdateOptions(options, subscription); - - Assert.False(options.AutomaticTax!.Enabled); - } -} diff --git a/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs b/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs deleted file mode 100644 index 2f3cbc98ee..0000000000 --- a/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Bit.Core.Billing.Tax.Services; -using Stripe; - -namespace Bit.Core.Test.Billing.Tax.Services; - -/// -/// Whether the subscription options will have automatic tax enabled or not. -/// -public class FakeAutomaticTaxStrategy( - bool isAutomaticTaxEnabled) : IAutomaticTaxStrategy -{ - public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription) - { - return new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled } - }; - } - - public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer) - { - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }; - } - - public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription) - { - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }; - } - - public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options) - { - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }; - - } -} diff --git a/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs deleted file mode 100644 index 30614b94ba..0000000000 --- a/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Tax.Services.Implementations; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Stripe; -using Xunit; - -namespace Bit.Core.Test.Billing.Tax.Services; - -[SutProviderCustomize] -public class PersonalUseAutomaticTaxStrategyTests -{ - [Theory] - [BitAutoData] - public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled( - SutProvider sutProvider) - { - var subscription = new Subscription(); - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(false); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.Null(actual); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Address = new Address - { - Country = "US", - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.Null(actual); - } - - [Theory] - [BitAutoData] - public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid( - SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = true - }, - Customer = new Customer - { - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.False(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData("CA")] - [BitAutoData("ES")] - [BitAutoData("US")] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAllCountries( - string country, SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = country - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.True(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData("CA")] - [BitAutoData("ES")] - [BitAutoData("US")] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds( - string country, SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = country, - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List - { - new() - { - Country = "ES", - Type = "eu_vat", - Value = "ESZ8880999Z" - } - } - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.True(actual.AutomaticTax.Enabled); - } - - [Theory] - [BitAutoData("CA")] - [BitAutoData("ES")] - [BitAutoData("US")] - public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds( - string country, SutProvider sutProvider) - { - var subscription = new Subscription - { - AutomaticTax = new SubscriptionAutomaticTax - { - Enabled = false - }, - Customer = new Customer - { - Address = new Address - { - Country = country - }, - Tax = new CustomerTax - { - AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported - }, - TaxIds = new StripeList - { - Data = new List() - } - } - }; - - sutProvider.GetDependency() - .IsEnabled(Arg.Is(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates)) - .Returns(true); - - var actual = sutProvider.Sut.GetUpdateOptions(subscription); - - Assert.NotNull(actual); - Assert.True(actual.AutomaticTax.Enabled); - } -} diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 7d8a059d76..609437b8d1 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,12 +1,10 @@ -using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Test.Billing.Tax.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -23,10 +21,6 @@ public class StripePaymentServiceTests public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage( SutProvider sutProvider) { - sutProvider.GetDependency() - .CreateAsync(Arg.Is(p => p.PlanType == PlanType.FamiliesAnnually)) - .Returns(new FakeAutomaticTaxStrategy(true)); - var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) @@ -74,10 +68,6 @@ public class StripePaymentServiceTests public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage( SutProvider sutProvider) { - sutProvider.GetDependency() - .CreateAsync(Arg.Is(p => p.PlanType == PlanType.FamiliesAnnually)) - .Returns(new FakeAutomaticTaxStrategy(true)); - var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) @@ -125,10 +115,6 @@ public class StripePaymentServiceTests public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage( SutProvider sutProvider) { - sutProvider.GetDependency() - .CreateAsync(Arg.Is(p => p.PlanType == PlanType.FamiliesAnnually)) - .Returns(new FakeAutomaticTaxStrategy(true)); - var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) @@ -177,10 +163,6 @@ public class StripePaymentServiceTests public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage( SutProvider sutProvider) { - sutProvider.GetDependency() - .CreateAsync(Arg.Is(p => p.PlanType == PlanType.FamiliesAnnually)) - .Returns(new FakeAutomaticTaxStrategy(true)); - var familiesPlan = new FamiliesPlan(); sutProvider.GetDependency() .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) @@ -223,4 +205,340 @@ public class StripePaymentServiceTests Assert.Equal(4.08M, actual.TotalAmount); Assert.Equal(4M, actual.TaxableBaseAmount); } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) + { + // Arrange + var familiesPlan = new FamiliesPlan(); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) + .Returns(familiesPlan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "US", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) + { + // Arrange + var plan = new EnterprisePlan(true); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) + .Returns(plan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.EnterpriseAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "US", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) + { + // Arrange + var familiesPlan = new FamiliesPlan(); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) + .Returns(familiesPlan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "FR", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) + { + // Arrange + var plan = new EnterprisePlan(true); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) + .Returns(plan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.EnterpriseAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "FR", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) + { + // Arrange + var familiesPlan = new FamiliesPlan(); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) + .Returns(familiesPlan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "US", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider sutProvider) + { + // Arrange + var plan = new EnterprisePlan(true); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) + .Returns(plan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.EnterpriseAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "US", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) + { + // Arrange + var familiesPlan = new FamiliesPlan(); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) + .Returns(familiesPlan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.FamiliesAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "FR", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == null + )); + } + + [Theory] + [BitAutoData] + public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider sutProvider) + { + // Arrange + var plan = new EnterprisePlan(true); + sutProvider.GetDependency() + .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) + .Returns(plan); + + var parameters = new PreviewOrganizationInvoiceRequestBody + { + PasswordManager = new OrganizationPasswordManagerRequestModel + { + Plan = PlanType.EnterpriseAnnually + }, + TaxInformation = new TaxInformationRequestModel + { + Country = "FR", + PostalCode = "12345" + } + }; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter + .InvoiceCreatePreviewAsync(Arg.Any()) + .Returns(new Invoice + { + TotalExcludingTax = 400, + Tax = 8, + Total = 408 + }); + + // Act + await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); + + // Assert + await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse + )); + } } From 3731c7c40c3e7da515328318e2b64983cd96d4f6 Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Wed, 3 Sep 2025 10:39:12 -0500 Subject: [PATCH 191/326] PM-24436 Add logging to backend for Member Access Report (#6159) * pm-24436 inital commit * PM-24436 updating logsto bypass event filter --- src/Api/Dirt/Controllers/ReportsController.cs | 32 ++++++++----------- .../ReportFeatures/MemberAccessReportQuery.cs | 21 +++++++++++- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index e7c7e4a9bf..d643d68661 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -1,6 +1,7 @@ using Bit.Api.Dirt.Models; using Bit.Api.Dirt.Models.Response; using Bit.Api.Tools.Models.Response; +using Bit.Core; using Bit.Core.Context; using Bit.Core.Dirt.Entities; using Bit.Core.Dirt.Reports.Models.Data; @@ -26,6 +27,7 @@ public class ReportsController : Controller private readonly IAddOrganizationReportCommand _addOrganizationReportCommand; private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand; private readonly IGetOrganizationReportQuery _getOrganizationReportQuery; + private readonly ILogger _logger; public ReportsController( ICurrentContext currentContext, @@ -36,7 +38,8 @@ public class ReportsController : Controller IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand, IGetOrganizationReportQuery getOrganizationReportQuery, IAddOrganizationReportCommand addOrganizationReportCommand, - IDropOrganizationReportCommand dropOrganizationReportCommand + IDropOrganizationReportCommand dropOrganizationReportCommand, + ILogger logger ) { _currentContext = currentContext; @@ -48,6 +51,7 @@ public class ReportsController : Controller _getOrganizationReportQuery = getOrganizationReportQuery; _addOrganizationReportCommand = addOrganizationReportCommand; _dropOrganizationReportCommand = dropOrganizationReportCommand; + _logger = logger; } /// @@ -86,32 +90,24 @@ public class ReportsController : Controller { if (!await _currentContext.AccessReports(orgId)) { + _logger.LogInformation(Constants.BypassFiltersEventId, + "AccessReports Check - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}", + _currentContext.UserId, orgId, _currentContext.DeviceType); throw new NotFoundException(); } - var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId }); + _logger.LogInformation(Constants.BypassFiltersEventId, + "MemberAccessReportQuery starts - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}", + _currentContext.UserId, orgId, _currentContext.DeviceType); + + var accessDetails = await _memberAccessReportQuery + .GetMemberAccessReportsAsync(new MemberAccessReportRequest { OrganizationId = orgId }); var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x)); return responses; } - /// - /// Contains the organization member info, the cipher ids associated with the member, - /// and details on their collections, groups, and permissions - /// - /// Request parameters - /// - /// List of a user's permissions at a group and collection level as well as the number of ciphers - /// associated with that group/collection - /// - private async Task> GetMemberAccessDetails( - MemberAccessReportRequest request) - { - var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request); - return accessDetails; - } - /// /// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids /// diff --git a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs index 33acd73d14..83d074454d 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessReportQuery.cs @@ -7,25 +7,40 @@ using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; using Bit.Core.Dirt.Reports.Repositories; using Bit.Core.Services; +using Microsoft.Extensions.Logging; namespace Bit.Core.Dirt.Reports.ReportFeatures; public class MemberAccessReportQuery( IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IApplicationCacheService applicationCacheService) : IMemberAccessReportQuery + IApplicationCacheService applicationCacheService, + ILogger logger) : IMemberAccessReportQuery { public async Task> GetMemberAccessReportsAsync( MemberAccessReportRequest request) { + logger.LogInformation(Constants.BypassFiltersEventId, "Starting MemberAccessReport generation for OrganizationId: {OrganizationId}", request.OrganizationId); + var baseDetails = await organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId( request.OrganizationId); + logger.LogInformation(Constants.BypassFiltersEventId, "Retrieved {BaseDetailsCount} base details for OrganizationId: {OrganizationId}", + baseDetails.Count(), request.OrganizationId); + var orgUsers = baseDetails.Select(x => x.UserGuid.GetValueOrDefault()).Distinct(); + var orgUsersCount = orgUsers.Count(); + logger.LogInformation(Constants.BypassFiltersEventId, "Found {UniqueUsersCount} unique users for OrganizationId: {OrganizationId}", + orgUsersCount, request.OrganizationId); + var orgUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); + logger.LogInformation(Constants.BypassFiltersEventId, "Retrieved two-factor status for {UsersCount} users for OrganizationId: {OrganizationId}", + orgUsersTwoFactorEnabled.Count(), request.OrganizationId); var orgAbility = await applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId); + logger.LogInformation(Constants.BypassFiltersEventId, "Retrieved organization ability (UseResetPassword: {UseResetPassword}) for OrganizationId: {OrganizationId}", + orgAbility?.UseResetPassword, request.OrganizationId); var accessDetails = baseDetails .GroupBy(b => new @@ -62,6 +77,10 @@ public class MemberAccessReportQuery( CipherIds = g.Select(c => c.CipherId) }); + var accessDetailsCount = accessDetails.Count(); + logger.LogInformation(Constants.BypassFiltersEventId, "Completed MemberAccessReport generation for OrganizationId: {OrganizationId}. Generated {AccessDetailsCount} access detail records", + request.OrganizationId, accessDetailsCount); + return accessDetails; } } From 93f4666df4b23e7292f456d656187c6dd587da06 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:42:19 -0500 Subject: [PATCH 192/326] [PM-25419] Move `ProviderPriceAdapter` to Core project (#6278) * Move ProviderPriceAdapter to Core * Run dotnet format --- .../Providers/Services/BusinessUnitConverterTests.cs | 1 + .../Providers/Services/ProviderBillingServiceTests.cs | 1 + .../Providers/Services/ProviderPriceAdapterTests.cs | 4 ++-- src/Api/Billing/Controllers/ProviderBillingController.cs | 1 - .../Billing/Providers/Services/ProviderPriceAdapter.cs | 8 +++----- .../Billing/Controllers/ProviderBillingControllerTests.cs | 1 - 6 files changed, 7 insertions(+), 9 deletions(-) rename {bitwarden_license/src/Commercial.Core => src/Core}/Billing/Providers/Services/ProviderPriceAdapter.cs (95%) diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs index c27d990213..ec52650097 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/BusinessUnitConverterTests.cs @@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 2bb4c9dcca..4e811017f9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -15,6 +15,7 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs index 3087d5761c..8dcf7f6bbc 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderPriceAdapterTests.cs @@ -1,7 +1,7 @@ -using Bit.Commercial.Core.Billing.Providers.Services; -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Services; using Stripe; using Xunit; diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index c131ed7688..f7d0593812 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -3,7 +3,6 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Models; diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs b/src/Core/Billing/Providers/Services/ProviderPriceAdapter.cs similarity index 95% rename from bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs rename to src/Core/Billing/Providers/Services/ProviderPriceAdapter.cs index 8c55d31f2c..1346afe914 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs +++ b/src/Core/Billing/Providers/Services/ProviderPriceAdapter.cs @@ -1,12 +1,10 @@ // ReSharper disable SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault -#nullable enable using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.Billing; using Bit.Core.Billing.Enums; using Stripe; -namespace Bit.Commercial.Core.Billing.Providers.Services; +namespace Bit.Core.Billing.Providers.Services; public static class ProviderPriceAdapter { @@ -52,7 +50,7 @@ public static class ProviderPriceAdapter /// The plan type correlating to the desired Stripe price ID. /// A Stripe ID. /// Thrown when the provider's type is not or . - /// Thrown when the provided does not relate to a Stripe price ID. + /// Thrown when the provided does not relate to a Stripe price ID. public static string GetPriceId( Provider provider, Subscription subscription, @@ -104,7 +102,7 @@ public static class ProviderPriceAdapter /// The plan type correlating to the desired Stripe price ID. /// A Stripe ID. /// Thrown when the provider's type is not or . - /// Thrown when the provided does not relate to a Stripe price ID. + /// Thrown when the provided does not relate to a Stripe price ID. public static string GetActivePriceId( Provider provider, PlanType planType) diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 75f301ec9c..8c1dd60fb9 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,7 +1,6 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; From 0385347a3a78fe65dff950f1ec77a9a7f8cda38d Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Wed, 3 Sep 2025 15:27:01 -0400 Subject: [PATCH 193/326] refactor: remove feature-flag (#6252) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9ddbf5c600..058f4eac69 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -160,7 +160,6 @@ public static class FeatureFlagKeys 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"; - public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; From 4b79b98b316a4af6d292e5c9de05dee3cf4f231a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:47:56 +0200 Subject: [PATCH 194/326] [deps]: Update actions/create-github-app-token action to v2 (#6216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/repository-management.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index 18192ca0ad..ad80d5864c 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -82,7 +82,7 @@ jobs: version: ${{ inputs.version_number_override }} - name: Generate GH App token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} @@ -200,7 +200,7 @@ jobs: uses: bitwarden/gh-actions/azure-logout@main - name: Generate GH App token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 id: app-token with: app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }} From cdf1d7f074e3c1e4f60b87e2eb75c841aedd28d4 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Thu, 4 Sep 2025 08:05:11 -0600 Subject: [PATCH 195/326] Add stub for load test work (#6277) * Add stub for load test work * Satisfy linter * Adding required permission for linting --- .github/workflows/load-test.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/load-test.yml diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml new file mode 100644 index 0000000000..19aab89be3 --- /dev/null +++ b/.github/workflows/load-test.yml @@ -0,0 +1,13 @@ +name: Test Stub +on: + workflow_dispatch: + +jobs: + test: + permissions: + contents: read + name: Test + runs-on: ubuntu-24.04 + steps: + - name: Test + run: exit 0 From 96fe09af893006195e6ecc50e7f6c8b786dea25d Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:08:03 -0400 Subject: [PATCH 196/326] [PM-25415] move files into better place for code ownership (#6275) * chore: move files into better place for code ownership * fix: import correct namespace --- .../SecretsManager/Commands/Projects/CreateProjectCommand.cs | 2 +- .../src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs | 3 ++- .../Authorization/OrganizationClaimsExtensions.cs | 2 +- src/Api/SecretsManager/Controllers/ProjectsController.cs | 2 +- src/Api/SecretsManager/Controllers/SecretsController.cs | 2 +- src/Api/SecretsManager/Controllers/SecretsTrashController.cs | 2 +- src/Api/Startup.cs | 3 ++- src/Api/Utilities/ServiceCollectionExtensions.cs | 2 +- src/Core/AdminConsole/Models/Data/Permissions.cs | 2 +- src/Core/{ => Auth}/Identity/Claims.cs | 2 +- .../Identity/CustomIdentityServiceCollectionExtensions.cs | 0 src/Core/{ => Auth}/Identity/IdentityClientType.cs | 2 +- src/Core/{ => Auth}/IdentityServer/ApiScopes.cs | 2 +- .../ConfigureOpenIdConnectDistributedOptions.cs | 2 +- .../IdentityServer/DistributedCacheCookieManager.cs | 2 +- .../IdentityServer/DistributedCacheTicketDataFormatter.cs | 2 +- .../{ => Auth}/IdentityServer/DistributedCacheTicketStore.cs | 2 +- .../SendAccess/SendAccessClaimsPrincipalExtensions.cs | 2 +- src/Core/Context/CurrentContext.cs | 2 +- src/Core/Context/ICurrentContext.cs | 2 +- src/Core/Enums/AccessClientType.cs | 2 +- .../SelfHosted/SelfHostedSyncSponsorshipsCommand.cs | 2 +- src/Core/Platform/Push/Engines/RelayPushEngine.cs | 4 ++-- .../Platform/PushRegistration/RelayPushRegistrationService.cs | 4 ++-- .../Commands/Projects/Interfaces/ICreateProjectCommand.cs | 2 +- .../Services/Implementations/LaunchDarklyFeatureService.cs | 2 +- src/Core/Utilities/CoreHelpers.cs | 2 +- src/Events/Startup.cs | 2 +- src/Identity/IdentityServer/ApiResources.cs | 4 ++-- .../ClientProviders/InstallationClientProvider.cs | 2 +- .../IdentityServer/ClientProviders/InternalClientProvider.cs | 2 +- .../ClientProviders/OrganizationClientProvider.cs | 4 ++-- .../ClientProviders/SecretsManagerApiKeyProvider.cs | 2 +- .../IdentityServer/ClientProviders/UserClientProvider.cs | 2 +- src/Identity/IdentityServer/ProfileService.cs | 2 +- .../IdentityServer/RequestValidators/BaseRequestValidator.cs | 2 +- .../RequestValidators/CustomTokenRequestValidator.cs | 2 +- .../RequestValidators/SendAccess/SendAccessGrantValidator.cs | 2 +- .../SendAccess/SendEmailOtpRequestValidator.cs | 2 +- .../SendAccess/SendPasswordRequestValidator.cs | 2 +- .../IdentityServer/StaticClients/SendClientBuilder.cs | 4 ++-- src/Identity/Utilities/ServiceCollectionExtensions.cs | 4 ++-- src/Notifications/Startup.cs | 2 +- src/SharedWeb/Utilities/ServiceCollectionExtensions.cs | 2 -- .../SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs | 2 +- .../SendAccessGrantValidatorIntegrationTests.cs | 2 +- .../SendEmailOtpReqestValidatorIntegrationTests.cs | 2 +- .../SendPasswordRequestValidatorIntegrationTests.cs | 4 ++-- .../ClientProviders/InstallationClientProviderTests.cs | 2 +- .../ClientProviders/InternalClientProviderTests.cs | 2 +- .../SendAccess/SendAccessGrantValidatorTests.cs | 4 ++-- .../SendAccess/SendEmailOtpRequestValidatorTests.cs | 4 ++-- .../SendAccess/SendPasswordRequestValidatorTests.cs | 4 ++-- .../IdentityServer/SendPasswordRequestValidatorTests.cs | 4 ++-- 54 files changed, 65 insertions(+), 65 deletions(-) rename src/Core/{ => Auth}/Identity/Claims.cs (98%) rename src/Core/{ => Auth}/Identity/CustomIdentityServiceCollectionExtensions.cs (100%) rename src/Core/{ => Auth}/Identity/IdentityClientType.cs (75%) rename src/Core/{ => Auth}/IdentityServer/ApiScopes.cs (96%) rename src/Core/{ => Auth}/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs (97%) rename src/Core/{ => Auth}/IdentityServer/DistributedCacheCookieManager.cs (98%) rename src/Core/{ => Auth}/IdentityServer/DistributedCacheTicketDataFormatter.cs (98%) rename src/Core/{ => Auth}/IdentityServer/DistributedCacheTicketStore.cs (97%) diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs index 1a5fe07c21..9f37c35f78 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs @@ -1,9 +1,9 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Exceptions; -using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Entities; diff --git a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs index 546bbfb7c9..db574e71c5 100644 --- a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs +++ b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography.X509Certificates; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Settings; @@ -416,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider SPOptions = spOptions, SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme, SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme, - CookieManager = new IdentityServer.DistributedCacheCookieManager(), + CookieManager = new DistributedCacheCookieManager(), }; options.IdentityProviders.Add(idp); diff --git a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs index e21d153bab..a3af3669ac 100644 --- a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs +++ b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs @@ -1,9 +1,9 @@ #nullable enable using System.Security.Claims; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Identity; using Bit.Core.Models.Data; namespace Bit.Api.AdminConsole.Authorization; diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index 11b840accf..5dce032ece 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -4,10 +4,10 @@ using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Identity; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Entities; diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index e32d5cd581..e263b9747d 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -4,10 +4,10 @@ using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Identity; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; diff --git a/src/Api/SecretsManager/Controllers/SecretsTrashController.cs b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs index 275e76cc99..d791fa2341 100644 --- a/src/Api/SecretsManager/Controllers/SecretsTrashController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs @@ -1,8 +1,8 @@ using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Identity; using Bit.Core.SecretsManager.Commands.Trash.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 3a08c4fe8a..2d306c4435 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -13,7 +13,6 @@ using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; using Bit.Core.Auth.Entities; -using Bit.Core.IdentityServer; using Bit.SharedWeb.Health; using Microsoft.IdentityModel.Logging; using Microsoft.OpenApi.Models; @@ -33,6 +32,8 @@ using Bit.Core.Tools.ImportFeatures; using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Tools.SendFeatures; +using Bit.Core.Auth.IdentityServer; + #if !OSS using Bit.Commercial.Core.SecretsManager; diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 0d8c3dec38..b956fc73bb 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -2,7 +2,7 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.PhishingDomainFeatures; using Bit.Core.PhishingDomainFeatures.Interfaces; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/Models/Data/Permissions.cs b/src/Core/AdminConsole/Models/Data/Permissions.cs index def468f18d..75bf2db8c9 100644 --- a/src/Core/AdminConsole/Models/Data/Permissions.cs +++ b/src/Core/AdminConsole/Models/Data/Permissions.cs @@ -1,5 +1,5 @@ using System.Text.Json.Serialization; -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; namespace Bit.Core.Models.Data; diff --git a/src/Core/Identity/Claims.cs b/src/Core/Auth/Identity/Claims.cs similarity index 98% rename from src/Core/Identity/Claims.cs rename to src/Core/Auth/Identity/Claims.cs index 39a036f3f9..ac78e987ae 100644 --- a/src/Core/Identity/Claims.cs +++ b/src/Core/Auth/Identity/Claims.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Identity; +namespace Bit.Core.Auth.Identity; public static class Claims { diff --git a/src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs b/src/Core/Auth/Identity/CustomIdentityServiceCollectionExtensions.cs similarity index 100% rename from src/Core/Identity/CustomIdentityServiceCollectionExtensions.cs rename to src/Core/Auth/Identity/CustomIdentityServiceCollectionExtensions.cs diff --git a/src/Core/Identity/IdentityClientType.cs b/src/Core/Auth/Identity/IdentityClientType.cs similarity index 75% rename from src/Core/Identity/IdentityClientType.cs rename to src/Core/Auth/Identity/IdentityClientType.cs index 9c43007f25..113877135d 100644 --- a/src/Core/Identity/IdentityClientType.cs +++ b/src/Core/Auth/Identity/IdentityClientType.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Identity; +namespace Bit.Core.Auth.Identity; public enum IdentityClientType : byte { diff --git a/src/Core/IdentityServer/ApiScopes.cs b/src/Core/Auth/IdentityServer/ApiScopes.cs similarity index 96% rename from src/Core/IdentityServer/ApiScopes.cs rename to src/Core/Auth/IdentityServer/ApiScopes.cs index 77ccb5a58a..8836a168b6 100644 --- a/src/Core/IdentityServer/ApiScopes.cs +++ b/src/Core/Auth/IdentityServer/ApiScopes.cs @@ -1,6 +1,6 @@ using Duende.IdentityServer.Models; -namespace Bit.Core.IdentityServer; +namespace Bit.Core.Auth.IdentityServer; public static class ApiScopes { diff --git a/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs b/src/Core/Auth/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs similarity index 97% rename from src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs rename to src/Core/Auth/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs index 381f81dea5..5319539050 100644 --- a/src/Core/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs +++ b/src/Core/Auth/IdentityServer/ConfigureOpenIdConnectDistributedOptions.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Bit.Core.IdentityServer; +namespace Bit.Core.Auth.IdentityServer; public class ConfigureOpenIdConnectDistributedOptions : IPostConfigureOptions { diff --git a/src/Core/IdentityServer/DistributedCacheCookieManager.cs b/src/Core/Auth/IdentityServer/DistributedCacheCookieManager.cs similarity index 98% rename from src/Core/IdentityServer/DistributedCacheCookieManager.cs rename to src/Core/Auth/IdentityServer/DistributedCacheCookieManager.cs index a01ff63d8f..138aeaf7e8 100644 --- a/src/Core/IdentityServer/DistributedCacheCookieManager.cs +++ b/src/Core/Auth/IdentityServer/DistributedCacheCookieManager.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.IdentityServer; +namespace Bit.Core.Auth.IdentityServer; public class DistributedCacheCookieManager : ICookieManager { diff --git a/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs b/src/Core/Auth/IdentityServer/DistributedCacheTicketDataFormatter.cs similarity index 98% rename from src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs rename to src/Core/Auth/IdentityServer/DistributedCacheTicketDataFormatter.cs index ad3fdee6f0..565d02a838 100644 --- a/src/Core/IdentityServer/DistributedCacheTicketDataFormatter.cs +++ b/src/Core/Auth/IdentityServer/DistributedCacheTicketDataFormatter.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Caching.Distributed; -namespace Bit.Core.IdentityServer; +namespace Bit.Core.Auth.IdentityServer; public class DistributedCacheTicketDataFormatter : ISecureDataFormat { diff --git a/src/Core/IdentityServer/DistributedCacheTicketStore.cs b/src/Core/Auth/IdentityServer/DistributedCacheTicketStore.cs similarity index 97% rename from src/Core/IdentityServer/DistributedCacheTicketStore.cs rename to src/Core/Auth/IdentityServer/DistributedCacheTicketStore.cs index ddf66f04ec..675b0cd7a5 100644 --- a/src/Core/IdentityServer/DistributedCacheTicketStore.cs +++ b/src/Core/Auth/IdentityServer/DistributedCacheTicketStore.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.Caching.Distributed; -namespace Bit.Core.IdentityServer; +namespace Bit.Core.Auth.IdentityServer; public class DistributedCacheTicketStore : ITicketStore { diff --git a/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs index 7ae7355ba4..f944de381e 100644 --- a/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs +++ b/src/Core/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensions.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; namespace Bit.Core.Auth.UserFeatures.SendAccess; diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index 85c8a81523..e824a30a0e 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Identity; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs index 42843ce6d7..417e220ba2 100644 --- a/src/Core/Context/ICurrentContext.cs +++ b/src/Core/Context/ICurrentContext.cs @@ -3,9 +3,9 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Context; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.Settings; using Microsoft.AspNetCore.Http; diff --git a/src/Core/Enums/AccessClientType.cs b/src/Core/Enums/AccessClientType.cs index fb757c6dd6..c7336ee40d 100644 --- a/src/Core/Enums/AccessClientType.cs +++ b/src/Core/Enums/AccessClientType.cs @@ -1,4 +1,4 @@ -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; namespace Bit.Core.Enums; diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs index 76e7b6bb2a..9a995a9cf0 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs @@ -1,9 +1,9 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.IdentityServer; using Bit.Core.Entities; using Bit.Core.Exceptions; -using Bit.Core.IdentityServer; using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Response.OrganizationSponsorships; using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; diff --git a/src/Core/Platform/Push/Engines/RelayPushEngine.cs b/src/Core/Platform/Push/Engines/RelayPushEngine.cs index 66b0229315..cff077c850 100644 --- a/src/Core/Platform/Push/Engines/RelayPushEngine.cs +++ b/src/Core/Platform/Push/Engines/RelayPushEngine.cs @@ -1,6 +1,6 @@ -using Bit.Core.Context; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.IdentityServer; using Bit.Core.Models; using Bit.Core.Models.Api; using Bit.Core.Repositories; diff --git a/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs b/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs index 96a259ecf8..0925e92f64 100644 --- a/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs +++ b/src/Core/Platform/PushRegistration/RelayPushRegistrationService.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Platform.Push; using Bit.Core.Services; diff --git a/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs b/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs index db377e220e..a1793cc73a 100644 --- a/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs +++ b/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs @@ -1,4 +1,4 @@ -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; using Bit.Core.SecretsManager.Entities; namespace Bit.Core.SecretsManager.Commands.Projects.Interfaces; diff --git a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs index 1fb2348c5a..f118146cb1 100644 --- a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs +++ b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs @@ -1,8 +1,8 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.Identity; using Bit.Core.Context; -using Bit.Core.Identity; using Bit.Core.Settings; using Bit.Core.Utilities; using LaunchDarkly.Logging; diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 64a038be07..813eb6d1aa 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -16,10 +16,10 @@ using Azure.Storage.Queues.Models; using Bit.Core.AdminConsole.Context; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Billing.Enums; using Bit.Core.Context; using Bit.Core.Entities; -using Bit.Core.Identity; using Bit.Core.Settings; using Duende.IdentityModel; using Microsoft.AspNetCore.DataProtection; diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index b498bce229..fdeaad04b2 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,6 +1,6 @@ using System.Globalization; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Context; -using Bit.Core.IdentityServer; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs index 61f3dd10ba..d225a7ea33 100644 --- a/src/Identity/IdentityServer/ApiResources.cs +++ b/src/Identity/IdentityServer/ApiResources.cs @@ -1,5 +1,5 @@ -using Bit.Core.Identity; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.IdentityServer; using Duende.IdentityModel; using Duende.IdentityServer.Models; diff --git a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs index cfa0dee0e6..566b0395b8 100644 --- a/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/InstallationClientProvider.cs @@ -1,7 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Platform.Installations; using Duende.IdentityModel; using Duende.IdentityServer.Models; diff --git a/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs index 3cab275a8f..70c1e2e06a 100644 --- a/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/InternalClientProvider.cs @@ -1,7 +1,7 @@ #nullable enable using System.Diagnostics; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Settings; using Duende.IdentityModel; using Duende.IdentityServer.Models; diff --git a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs index 2bcae37ee2..86a1272496 100644 --- a/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/OrganizationClientProvider.cs @@ -1,9 +1,9 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.Repositories; using Duende.IdentityModel; using Duende.IdentityServer.Models; diff --git a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs index 11022a40e5..628163ae74 100644 --- a/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/SecretsManagerApiKeyProvider.cs @@ -1,7 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Repositories; diff --git a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs index 29d036b893..2d380acdf6 100644 --- a/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs +++ b/src/Identity/IdentityServer/ClientProviders/UserClientProvider.cs @@ -3,9 +3,9 @@ using System.Collections.ObjectModel; using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; using Bit.Core.Billing.Services; using Bit.Core.Context; -using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.Utilities; using Duende.IdentityModel; diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index 74173a7e9d..9ea8fcf471 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -1,9 +1,9 @@ using System.Security.Claims; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Identity; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 5a8cb8645e..e57ed1c85f 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -8,12 +8,12 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Identity; using Bit.Core.Models.Api; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index c7bf1a77db..1495973b80 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -3,11 +3,11 @@ using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; -using Bit.Core.IdentityServer; using Bit.Core.Platform.Installations; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs index 5fe0b7b724..2ecc5a9704 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -1,6 +1,6 @@ using System.Security.Claims; using Bit.Core; -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs index e26556eb80..ca48c4fbec 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendEmailOtpRequestValidator.cs @@ -1,6 +1,6 @@ using System.Security.Claims; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; -using Bit.Core.Identity; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Identity.IdentityServer.Enums; diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs index 4eade01a49..a514e3bc8b 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendPasswordRequestValidator.cs @@ -1,5 +1,5 @@ using System.Security.Claims; -using Bit.Core.Identity; +using Bit.Core.Auth.Identity; using Bit.Core.KeyManagement.Sends; using Bit.Core.Tools.Models.Data; using Bit.Identity.IdentityServer.Enums; diff --git a/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs b/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs index 7197d435ed..6424316505 100644 --- a/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs +++ b/src/Identity/IdentityServer/StaticClients/SendClientBuilder.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Enums; using Bit.Core.Settings; using Bit.Identity.IdentityServer.Enums; using Duende.IdentityServer.Models; diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 95c067d884..9d062e5c06 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -1,5 +1,5 @@ -using Bit.Core.Auth.Repositories; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Auth.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; diff --git a/src/Notifications/Startup.cs b/src/Notifications/Startup.cs index c939d0d2fd..eb3c3f8682 100644 --- a/src/Notifications/Startup.cs +++ b/src/Notifications/Startup.cs @@ -1,5 +1,5 @@ using System.Globalization; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 0dd5431dd7..4f0d0d4397 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -30,8 +30,6 @@ using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.HostedServices; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.KeyManagement; using Bit.Core.NotificationCenter; using Bit.Core.OrganizationFeatures; diff --git a/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs index bf5322d916..ac625dad9e 100644 --- a/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs +++ b/test/Core.Test/Auth/UserFeatures/SendAccess/SendAccessClaimsPrincipalExtensionsTests.cs @@ -1,6 +1,6 @@ using System.Security.Claims; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures.SendAccess; -using Bit.Core.Identity; using Xunit; namespace Bit.Core.Test.Auth.UserFeatures.SendAccess; diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs index 3b0cf2c282..ca6417d49c 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs @@ -1,6 +1,6 @@ using Bit.Core; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; -using Bit.Core.IdentityServer; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; diff --git a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs index 9d9bc03ef5..9a097cc061 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs @@ -1,6 +1,6 @@ using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; -using Bit.Core.IdentityServer; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; diff --git a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs index 232adb6884..856ffe1f6e 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs @@ -1,5 +1,5 @@ -using Bit.Core.Enums; -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Enums; using Bit.Core.KeyManagement.Sends; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; diff --git a/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs b/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs index b53e6ea15f..f9949c0c3a 100644 --- a/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs +++ b/test/Identity.Test/IdentityServer/ClientProviders/InstallationClientProviderTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Platform.Installations; using Bit.Identity.IdentityServer.ClientProviders; using Duende.IdentityModel; diff --git a/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs b/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs index 4e5e659218..dda48f2af3 100644 --- a/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs +++ b/test/Identity.Test/IdentityServer/ClientProviders/InternalClientProviderTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.IdentityServer; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Settings; using Bit.Identity.IdentityServer.ClientProviders; using Duende.IdentityModel; diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs index e651709c47..017ad70354 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs @@ -1,8 +1,8 @@ using System.Collections.Specialized; using Bit.Core; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs index 2fd21fd4cf..70a1585d8b 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs @@ -1,8 +1,8 @@ using System.Collections.Specialized; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs index e2b8b49830..e77626d37b 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs @@ -1,8 +1,8 @@ using System.Collections.Specialized; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.UserFeatures.SendAccess; using Bit.Core.Enums; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.KeyManagement.Sends; using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; diff --git a/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs index ccee33d8c7..2ad1039a98 100644 --- a/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendPasswordRequestValidatorTests.cs @@ -1,8 +1,8 @@ using System.Collections.Specialized; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.UserFeatures.SendAccess; using Bit.Core.Enums; -using Bit.Core.Identity; -using Bit.Core.IdentityServer; using Bit.Core.KeyManagement.Sends; using Bit.Core.Tools.Models.Data; using Bit.Core.Utilities; From e456b4ce219dd4ee166e98c415027ee4a445b537 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 4 Sep 2025 12:23:14 -0400 Subject: [PATCH 197/326] add feature flag (#6284) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 058f4eac69..57798204ea 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -128,6 +128,7 @@ public static class FeatureFlagKeys public const string CreateDefaultLocation = "pm-19467-create-default-location"; public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; + public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; From 8b30c33eaebf244661fb876006d63093bbdae2e1 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Thu, 4 Sep 2025 12:54:24 -0500 Subject: [PATCH 198/326] PM-25413 no badRequest result because of error from Onyx (#6285) --- .../Controllers/FreshdeskController.cs | 15 ++++-- .../Controllers/FreshdeskControllerTests.cs | 51 +++++++++++++++++-- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 3f26e28786..a854d2d49f 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -152,6 +152,12 @@ public class FreshdeskController : Controller return new BadRequestResult(); } + // if there is no description, then we don't send anything to onyx + if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim())) + { + return Ok(); + } + // create the onyx `answer-with-citation` request var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId); var onyxRequest = new HttpRequestMessage(HttpMethod.Post, @@ -164,9 +170,12 @@ public class FreshdeskController : Controller // the CallOnyxApi will return a null if we have an error response if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg)) { - return BadRequest( - string.Format("Failed to get a valid response from Onyx API. Response: {0}", - JsonSerializer.Serialize(onyxJsonResponse ?? new OnyxAnswerWithCitationResponseModel()))); + _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)); + + return Ok(); // return ok so we don't retry } // add the answer as a note to the ticket diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index f0a34ff232..8fd0769a02 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ReceivedExtensions; @@ -126,7 +127,7 @@ public class FreshdeskControllerTests [Theory] [BitAutoData(WebhookKey)] - public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest( + public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged( string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, SutProvider sutProvider) { @@ -150,8 +151,18 @@ public class FreshdeskControllerTests var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); - var result = Assert.IsAssignableFrom(response); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode); + var statusCodeResult = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); + + var _logger = sutProvider.GetDependency>(); + + // workaround because _logger.Received(1).LogWarning(...) does not work + _logger.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Log" && c.GetArguments()[1].ToString().Contains("Error getting answer from Onyx AI")); + + // sent call to Onyx API - but we got an error response + _ = mockOnyxHttpMessageHandler.Received(1).Send(Arg.Any(), Arg.Any()); + // did not call freshdesk to add a note since onyx failed + _ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Theory] @@ -174,10 +185,9 @@ public class FreshdeskControllerTests .Returns(mockFreshdeskAddNoteResponse); var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); - // mocking Onyx api response given a ticket description var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); - onyxResponse.ErrorMsg = string.Empty; + onyxResponse.ErrorMsg = "string.Empty"; var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(onyxResponse)) @@ -195,6 +205,37 @@ public class FreshdeskControllerTests Assert.Equal(StatusCodes.Status200OK, result.StatusCode); } + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhookOnyxAi_ticket_description_is_empty_return_success( + string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model, + SutProvider sutProvider) + { + var billingSettings = sutProvider.GetDependency>().Value; + billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey); + billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api"); + + model.TicketDescriptionText = " "; // empty description + + // mocking freshdesk api add note request (POST) + var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf(); + var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler); + + // mocking Onyx api response given a ticket description + var mockOnyxHttpMessageHandler = Substitute.ForPartsOf(); + var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler); + + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient); + sutProvider.GetDependency().CreateClient("OnyxApi").Returns(onyxHttpClient); + + var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model); + + var result = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status200OK, result.StatusCode); + _ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); + _ = mockOnyxHttpMessageHandler.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + public class MockHttpMessageHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) From 1b0be3e87f22644ee67dcfe9b0e199e264045738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:22:50 +0100 Subject: [PATCH 199/326] [PM-22839] Add SSO configuration fields to organization user details for hiding device approvals page (#6245) * Add SsoEnabled field to OrganizationUserOrganizationDetailsView - Updated OrganizationUserOrganizationDetailsViewQuery to include SsoEnabled property. - Modified SQL view to select SsoEnabled from SsoConfig. - Created migration script to alter the view and refresh dependent views. * Enhance OrganizationUserRepositoryTests to include SSO configuration - Added ISsoConfigRepository dependency to GetManyDetailsByUserAsync test. - Created SsoConfigurationData instance and integrated SSO configuration checks in assertions. - Updated tests to validate SSO-related properties in the response model. * Add SSO properties to ProfileOrganizationResponseModel and OrganizationUserOrganizationDetails - Introduced SsoEnabled and SsoMemberDecryptionType fields in ProfileOrganizationResponseModel. - Added SsoEnabled property to OrganizationUserOrganizationDetails for enhanced SSO configuration support. --- .../ProfileOrganizationResponseModel.cs | 4 + .../OrganizationUserOrganizationDetails.cs | 1 + ...izationUserOrganizationDetailsViewQuery.cs | 1 + ...rganizationUserOrganizationDetailsView.sql | 1 + .../OrganizationUserRepositoryTests.cs | 21 ++++- ...8-25_00_OrgUserOrgDetailsAddSsoEnabled.sql | 86 +++++++++++++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 util/Migrator/DbScripts/2025-08-25_00_OrgUserOrgDetailsAddSsoEnabled.sql diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index e421c3247e..fd2bfe06dc 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -78,12 +78,14 @@ public class ProfileOrganizationResponseModel : ResponseModel UseRiskInsights = organization.UseRiskInsights; UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; + SsoEnabled = organization.SsoEnabled ?? false; if (organization.SsoConfig != null) { var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig); KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl); KeyConnectorUrl = ssoConfigData.KeyConnectorUrl; + SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType; } } @@ -160,4 +162,6 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool IsAdminInitiated { get; set; } + public bool SsoEnabled { get; set; } + public MemberDecryptionType? SsoMemberDecryptionType { 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 bad06ccf64..b7e573c4e6 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -49,6 +49,7 @@ public class OrganizationUserOrganizationDetails public string ProviderName { get; set; } public ProviderType? ProviderType { get; set; } public string FamilySponsorshipFriendlyName { get; set; } + public bool? SsoEnabled { get; set; } public string SsoConfig { get; set; } public DateTime? FamilySponsorshipLastSyncDate { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 71bf113416..26d3a128fc 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -56,6 +56,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery Date: Fri, 5 Sep 2025 12:01:14 +0100 Subject: [PATCH 200/326] [PM-21752] Add granular events for collection management settings (#6269) * Add new event types for collection management settings in EventType enum * Refactor collection management settings update process in OrganizationsController and IOrganizationService. Introduced UpdateCollectionManagementSettingsAsync method to streamline updates and logging for collection management settings. * Add unit tests for collection management settings updates in OrganizationsController and OrganizationService. Implemented tests to verify the successful update of collection management settings and the logging of specific events when settings are changed. Added error handling for cases where the organization is not found. * Refactor collection management settings handling in OrganizationsController and IOrganizationService. Updated the UpdateCollectionManagementSettingsAsync method to accept a single settings object, simplifying the parameter list and improving code readability. Introduced a new OrganizationCollectionManagementSettings model to encapsulate collection management settings. Adjusted related tests to reflect these changes. * Add Obsolete attribute to Organization_CollectionManagement_Updated event in EventType enum --- .../Controllers/OrganizationsController.cs | 8 +- ...nCollectionManagementUpdateRequestModel.cs | 16 ++- src/Core/AdminConsole/Enums/EventType.cs | 11 +- ...rganizationCollectionManagementSettings.cs | 9 ++ .../Services/IOrganizationService.cs | 4 +- .../Implementations/OrganizationService.cs | 74 +++++++++++- .../OrganizationsControllerTests.cs | 39 +++++++ .../Services/OrganizationServiceTests.cs | 107 +++++++++++++++++- 8 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Business/OrganizationCollectionManagementSettings.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 8b1a6243c3..17e6a60cd9 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -554,18 +554,12 @@ public class OrganizationsController : Controller [HttpPut("{id}/collection-management")] public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) { - var organization = await _organizationRepository.GetByIdAsync(id); - if (organization == null) - { - throw new NotFoundException(); - } - if (!await _currentContext.OrganizationOwner(id)) { throw new NotFoundException(); } - await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated); + var organization = await _organizationService.UpdateCollectionManagementSettingsAsync(id, model.ToSettings()); var plan = await _pricingClient.GetPlan(organization.PlanType); return new OrganizationResponseModel(organization, plan); } diff --git a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs index 829840c896..93866161c0 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCollectionManagementUpdateRequestModel.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Services; +using Bit.Core.AdminConsole.Models.Business; namespace Bit.Api.Models.Request.Organizations; @@ -10,12 +9,11 @@ public class OrganizationCollectionManagementUpdateRequestModel public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } - public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService) + public OrganizationCollectionManagementSettings ToSettings() => new() { - existingOrganization.LimitCollectionCreation = LimitCollectionCreation; - existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion; - existingOrganization.LimitItemDeletion = LimitItemDeletion; - existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; - return existingOrganization; - } + LimitCollectionCreation = LimitCollectionCreation, + LimitCollectionDeletion = LimitCollectionDeletion, + LimitItemDeletion = LimitItemDeletion, + AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems + }; } diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/AdminConsole/Enums/EventType.cs index 32ea4a64e9..81501fd6ec 100644 --- a/src/Core/AdminConsole/Enums/EventType.cs +++ b/src/Core/AdminConsole/Enums/EventType.cs @@ -70,7 +70,16 @@ public enum EventType : int Organization_EnabledKeyConnector = 1606, Organization_DisabledKeyConnector = 1607, Organization_SponsorshipsSynced = 1608, - Organization_CollectionManagement_Updated = 1609, + [Obsolete("Use other specific Organization_CollectionManagement events instead")] + Organization_CollectionManagement_Updated = 1609, // TODO: Will be removed in PM-25315 + Organization_CollectionManagement_LimitCollectionCreationEnabled = 1610, + Organization_CollectionManagement_LimitCollectionCreationDisabled = 1611, + Organization_CollectionManagement_LimitCollectionDeletionEnabled = 1612, + Organization_CollectionManagement_LimitCollectionDeletionDisabled = 1613, + Organization_CollectionManagement_LimitItemDeletionEnabled = 1614, + Organization_CollectionManagement_LimitItemDeletionDisabled = 1615, + Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled = 1616, + Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617, Policy_Updated = 1700, diff --git a/src/Core/AdminConsole/Models/Business/OrganizationCollectionManagementSettings.cs b/src/Core/AdminConsole/Models/Business/OrganizationCollectionManagementSettings.cs new file mode 100644 index 0000000000..aff2244598 --- /dev/null +++ b/src/Core/AdminConsole/Models/Business/OrganizationCollectionManagementSettings.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.AdminConsole.Models.Business; + +public record OrganizationCollectionManagementSettings +{ + public bool LimitCollectionCreation { get; set; } + public bool LimitCollectionDeletion { get; set; } + public bool LimitItemDeletion { get; set; } + public bool AllowAdminAccessToAllCollectionItems { get; set; } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 8c47ae049c..94df74afdf 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -2,6 +2,7 @@ #nullable disable using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -19,7 +20,8 @@ public interface IOrganizationService Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); - Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); + Task UpdateAsync(Organization organization, bool updateBilling = false); + Task UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index f418737508..57eb4f51de 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -378,8 +379,7 @@ public class OrganizationService : IOrganizationService } } - public async Task UpdateAsync(Organization organization, bool updateBilling = false, - EventType eventType = EventType.Organization_Updated) + public async Task UpdateAsync(Organization organization, bool updateBilling = false) { if (organization.Id == default(Guid)) { @@ -395,7 +395,7 @@ public class OrganizationService : IOrganizationService } } - await ReplaceAndUpdateCacheAsync(organization, eventType); + await ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) { @@ -420,11 +420,35 @@ public class OrganizationService : IOrganizationService }, }); } + } - if (eventType == EventType.Organization_CollectionManagement_Updated) + public async Task UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings) + { + var existingOrganization = await _organizationRepository.GetByIdAsync(organizationId); + if (existingOrganization == null) { - await _pushNotificationService.PushSyncOrganizationCollectionManagementSettingsAsync(organization); + throw new NotFoundException(); } + + // Create logging actions based on what will change + var loggingActions = CreateCollectionManagementLoggingActions(existingOrganization, settings); + + existingOrganization.LimitCollectionCreation = settings.LimitCollectionCreation; + existingOrganization.LimitCollectionDeletion = settings.LimitCollectionDeletion; + existingOrganization.LimitItemDeletion = settings.LimitItemDeletion; + existingOrganization.AllowAdminAccessToAllCollectionItems = settings.AllowAdminAccessToAllCollectionItems; + existingOrganization.RevisionDate = DateTime.UtcNow; + + await ReplaceAndUpdateCacheAsync(existingOrganization); + + if (loggingActions.Any()) + { + await Task.WhenAll(loggingActions.Select(action => action())); + } + + await _pushNotificationService.PushSyncOrganizationCollectionManagementSettingsAsync(existingOrganization); + + return existingOrganization; } public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type) @@ -1214,4 +1238,44 @@ public class OrganizationService : IOrganizationService return status; } + + private List> CreateCollectionManagementLoggingActions( + Organization existingOrganization, OrganizationCollectionManagementSettings settings) + { + var loggingActions = new List>(); + + if (existingOrganization.LimitCollectionCreation != settings.LimitCollectionCreation) + { + var eventType = settings.LimitCollectionCreation + ? EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled + : EventType.Organization_CollectionManagement_LimitCollectionCreationDisabled; + loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType)); + } + + if (existingOrganization.LimitCollectionDeletion != settings.LimitCollectionDeletion) + { + var eventType = settings.LimitCollectionDeletion + ? EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled + : EventType.Organization_CollectionManagement_LimitCollectionDeletionDisabled; + loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType)); + } + + if (existingOrganization.LimitItemDeletion != settings.LimitItemDeletion) + { + var eventType = settings.LimitItemDeletion + ? EventType.Organization_CollectionManagement_LimitItemDeletionEnabled + : EventType.Organization_CollectionManagement_LimitItemDeletionDisabled; + loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType)); + } + + if (existingOrganization.AllowAdminAccessToAllCollectionItems != settings.AllowAdminAccessToAllCollectionItems) + { + var eventType = settings.AllowAdminAccessToAllCollectionItems + ? EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled + : EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled; + loggingActions.Add(() => _eventService.LogOrganizationEventAsync(existingOrganization, eventType)); + } + + return loggingActions; + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 3484c9a995..00fd3c3b4e 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -2,10 +2,12 @@ using AutoFixture.Xunit2; using Bit.Api.AdminConsole.Controllers; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Models.Request.Organizations; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -29,6 +31,7 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tokens; +using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using NSubstitute; using Xunit; @@ -293,4 +296,40 @@ public class OrganizationsControllerTests : IDisposable Assert.True(result.ResetPasswordEnabled); } + + [Theory, AutoData] + public async Task PutCollectionManagement_ValidRequest_Success( + Organization organization, + OrganizationCollectionManagementUpdateRequestModel model) + { + // Arrange + _currentContext.OrganizationOwner(organization.Id).Returns(true); + + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + _pricingClient.GetPlan(Arg.Any()).Returns(plan); + + _organizationService + .UpdateCollectionManagementSettingsAsync( + organization.Id, + Arg.Is(s => + s.LimitCollectionCreation == model.LimitCollectionCreation && + s.LimitCollectionDeletion == model.LimitCollectionDeletion && + s.LimitItemDeletion == model.LimitItemDeletion && + s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems)) + .Returns(organization); + + // Act + await _sut.PutCollectionManagement(organization.Id, model); + + // Assert + await _organizationService + .Received(1) + .UpdateCollectionManagementSettingsAsync( + organization.Id, + Arg.Is(s => + s.LimitCollectionCreation == model.LimitCollectionCreation && + s.LimitCollectionDeletion == model.LimitCollectionDeletion && + s.LimitItemDeletion == model.LimitItemDeletion && + s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems)); + } } diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index e3f26a898d..33f2e78799 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; @@ -27,7 +28,6 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ExceptionExtensions; -using NSubstitute.ReceivedExtensions; using NSubstitute.ReturnsExtensions; using Stripe; using Xunit; @@ -42,8 +42,6 @@ public class OrganizationServiceTests { private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); - - [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData] @@ -1229,6 +1227,109 @@ public class OrganizationServiceTests .GetByIdentifierAsync(Arg.Is(id => id == organization.Identifier)); } + [Theory] + [BitAutoData(false, true, false, true)] + [BitAutoData(true, false, true, false)] + public async Task UpdateCollectionManagementSettingsAsync_WhenSettingsChanged_LogsSpecificEvents( + bool newLimitCollectionCreation, + bool newLimitCollectionDeletion, + bool newLimitItemDeletion, + bool newAllowAdminAccessToAllCollectionItems, + Organization existingOrganization, SutProvider sutProvider) + { + // Arrange + existingOrganization.LimitCollectionCreation = false; + existingOrganization.LimitCollectionDeletion = false; + existingOrganization.LimitItemDeletion = false; + existingOrganization.AllowAdminAccessToAllCollectionItems = false; + + sutProvider.GetDependency() + .GetByIdAsync(existingOrganization.Id) + .Returns(existingOrganization); + + var settings = new OrganizationCollectionManagementSettings + { + LimitCollectionCreation = newLimitCollectionCreation, + LimitCollectionDeletion = newLimitCollectionDeletion, + LimitItemDeletion = newLimitItemDeletion, + AllowAdminAccessToAllCollectionItems = newAllowAdminAccessToAllCollectionItems + }; + + // Act + await sutProvider.Sut.UpdateCollectionManagementSettingsAsync(existingOrganization.Id, settings); + + // Assert + var eventService = sutProvider.GetDependency(); + if (newLimitCollectionCreation) + { + await eventService.Received(1).LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled)); + } + else + { + await eventService.DidNotReceive().LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled)); + } + + if (newLimitCollectionDeletion) + { + await eventService.Received(1).LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled)); + } + else + { + await eventService.DidNotReceive().LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled)); + } + + if (newLimitItemDeletion) + { + await eventService.Received(1).LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled)); + } + else + { + await eventService.DidNotReceive().LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled)); + } + + if (newAllowAdminAccessToAllCollectionItems) + { + await eventService.Received(1).LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled)); + } + else + { + await eventService.DidNotReceive().LogOrganizationEventAsync( + Arg.Is(org => org.Id == existingOrganization.Id), + Arg.Is(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled)); + } + } + + [Theory, BitAutoData] + public async Task UpdateCollectionManagementSettingsAsync_WhenOrganizationNotFound_ThrowsNotFoundException( + Guid organizationId, OrganizationCollectionManagementSettings settings, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns((Organization)null); + + // Act/Assert + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateCollectionManagementSettingsAsync(organizationId, settings)); + + await sutProvider.GetDependency() + .Received(1) + .GetByIdAsync(organizationId); + } + // Must set real guids in order for dictionary of guids to not throw aggregate exceptions private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository) { From 6d4129c6b7db3c672207d2ba1304297865cfe712 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:36:01 -0400 Subject: [PATCH 201/326] [PM-20595] Add Policy for Send access (#6282) * feat: add policy to API startup and Policies class to hold the static strings * test: add snapshot testing for constants to help with rust mappings * doc: add docs for send access --- src/Api/Startup.cs | 7 ++ src/Core/Auth/Identity/Policies.cs | 10 +++ .../SendAccess/SendAccessConstants.cs | 4 +- .../SendAccess/SendAccessGrantValidator.cs | 6 +- .../RequestValidators/SendAccess/readme.md | 66 +++++++++++++++++ .../SendAccess/SendConstantsSnapshotTests.cs | 73 +++++++++++++++++++ 6 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/Core/Auth/Identity/Policies.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md create mode 100644 test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 2d306c4435..1d5a1609f4 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -33,6 +33,7 @@ using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Tools.SendFeatures; using Bit.Core.Auth.IdentityServer; +using Bit.Core.Auth.Identity; #if !OSS @@ -145,6 +146,12 @@ public class Startup (c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets)) )); }); + config.AddPolicy(Policies.Send, configurePolicy: policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiSendAccess); + policy.RequireClaim(Claims.SendAccessClaims.SendId); + }); }); services.AddScoped(); diff --git a/src/Core/Auth/Identity/Policies.cs b/src/Core/Auth/Identity/Policies.cs new file mode 100644 index 0000000000..78d86d06a4 --- /dev/null +++ b/src/Core/Auth/Identity/Policies.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Auth.Identity; + +public static class Policies +{ + /// + /// Policy for managing access to the Send feature. + /// + public const string Send = "Send"; // [Authorize(Policy = Policies.Send)] + // TODO: migrate other existing policies to use this class +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs index fae7ba4215..17ec387411 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs @@ -5,6 +5,8 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; /// /// String constants for the Send Access user feature +/// Most of these need to be synced with the `bitwarden-auth` crate in the SDK. +/// There is snapshot testing to help ensure this. /// public static class SendAccessConstants { @@ -41,7 +43,7 @@ public static class SendAccessConstants /// /// The sendId is missing from the request. /// - public const string MissingSendId = "send_id_required"; + public const string SendIdRequired = "send_id_required"; /// /// The sendId is invalid, does not match a known send. /// diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs index 2ecc5a9704..d9ae946d16 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -23,7 +23,7 @@ public class SendAccessGrantValidator( private static readonly Dictionary _sendGrantValidatorErrorDescriptions = new() { - { SendAccessConstants.GrantValidatorResults.MissingSendId, $"{SendAccessConstants.TokenRequest.SendId} is required." }, + { SendAccessConstants.GrantValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." }, { SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." } }; @@ -90,7 +90,7 @@ public class SendAccessGrantValidator( // if the sendId is null then the request is the wrong shape and the request is invalid if (sendId == null) { - return (Guid.Empty, SendAccessConstants.GrantValidatorResults.MissingSendId); + return (Guid.Empty, SendAccessConstants.GrantValidatorResults.SendIdRequired); } // the send_id is not null so the request is the correct shape, so we will attempt to parse it try @@ -125,7 +125,7 @@ public class SendAccessGrantValidator( return error switch { // Request is the wrong shape - SendAccessConstants.GrantValidatorResults.MissingSendId => new GrantValidationResult( + SendAccessConstants.GrantValidatorResults.SendIdRequired => new GrantValidationResult( TokenRequestErrors.InvalidRequest, errorDescription: _sendGrantValidatorErrorDescriptions[error], customResponse), diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md b/src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md new file mode 100644 index 0000000000..afab13a156 --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/readme.md @@ -0,0 +1,66 @@ +Send Access Request Validation +=== + +This feature supports the ability of Tools to require specific claims for access to sends. + +In order to access Send data a user must meet the requirements laid out in these request validators. + +# ***Important: String Constants*** + +The string constants contained herein are used in conjunction with the Auth module in the SDK. Any change to these string values _must_ be intentional and _must_ have a corresponding change in the SDK. + +There is snapshot testing that will fail if the strings change to help detect unintended changes to the string constants. + +# Custom Claims + +Send access tokens contain custom claims specific to the Send the Send grant type. + +1. `send_id` - is always included in the issued access token. This is the `GUID` of the request Send. +1. `send_email` - only set when the Send requires `EmailOtp` authentication type. +1. `type` - this will always be `Send` + +# Authentication methods + +## `NeverAuthenticate` + +For a Send to be in this state two things can be true: +1. The Send has been modified and no longer allows access. +2. The Send does not exist. + +## `NotAuthenticated` + +In this scenario the Send is not protected by any added authentication or authorization and the access token is issued to the requesting user. + +## `ResourcePassword` + +In this scenario the Send is password protected and a user must supply the correct password hash to be issued an access token. + +## `EmailOtp` + +In this scenario the Send is only accessible to owners of specific email addresses. The user must submit a correct email. Once the email has been entered then ownership of the email must be established via OTP. The Otp is sent to the aforementioned email and must be supplied, along with the email, to be issued an access token. + +# Send Access Request Validation + +## Required Parameters + +### All Requests +- `send_id` - Base64 URL-encoded GUID of the send being accessed + +### Password Protected Sends +- `password_hash_b64` - client hashed Base64-encoded password. + +### Email OTP Protected Sends +- `email` - Email address associated with the send +- `otp` - One-time password (optional - if missing, OTP is generated and sent) + +## Error Responses + +All errors include a custom response field: +```json +{ + "error": "invalid_request|invalid_grant", + "error_description": "Human readable description", + "send_access_error_type": "specific_error_code" +} +``` + diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs new file mode 100644 index 0000000000..95a0a6675b --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs @@ -0,0 +1,73 @@ +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer.SendAccess; + +/// +/// Snapshot tests to ensure the string constants in do not change unintentionally. +/// If you change any of these values, please ensure you understand the impact and update the SDK accordingly. +/// If you intentionally change any of these values, please update the tests to reflect the new expected values. +/// +public class SendConstantsSnapshotTests +{ + [Fact] + public void SendAccessError_Constant_HasCorrectValue() + { + // Assert + Assert.Equal("send_access_error_type", SendAccessConstants.SendAccessError); + } + + [Fact] + public void TokenRequest_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("send_id", SendAccessConstants.TokenRequest.SendId); + Assert.Equal("password_hash_b64", SendAccessConstants.TokenRequest.ClientB64HashedPassword); + Assert.Equal("email", SendAccessConstants.TokenRequest.Email); + Assert.Equal("otp", SendAccessConstants.TokenRequest.Otp); + } + + [Fact] + public void GrantValidatorResults_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid); + Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired); + Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId); + } + + [Fact] + public void PasswordValidatorResults_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("password_hash_b64_invalid", SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch); + Assert.Equal("password_hash_b64_required", SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired); + } + + [Fact] + public void EmailOtpValidatorResults_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("email_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailInvalid); + Assert.Equal("email_required", SendAccessConstants.EmailOtpValidatorResults.EmailRequired); + Assert.Equal("email_and_otp_required_otp_sent", SendAccessConstants.EmailOtpValidatorResults.EmailOtpSent); + Assert.Equal("otp_invalid", SendAccessConstants.EmailOtpValidatorResults.EmailOtpInvalid); + Assert.Equal("otp_generation_failed", SendAccessConstants.EmailOtpValidatorResults.OtpGenerationFailed); + } + + [Fact] + public void OtpToken_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("send_access", SendAccessConstants.OtpToken.TokenProviderName); + Assert.Equal("email_otp", SendAccessConstants.OtpToken.Purpose); + Assert.Equal("{0}_{1}", SendAccessConstants.OtpToken.TokenUniqueIdentifier); + } + + [Fact] + public void OtpEmail_Constants_HaveCorrectValues() + { + // Assert + Assert.Equal("Your Bitwarden Send verification code is {0}", SendAccessConstants.OtpEmail.Subject); + } +} From 87bc9299e6fb4c95e420d74f2af9eb4e46400ac0 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Fri, 5 Sep 2025 11:15:01 -0400 Subject: [PATCH 202/326] [PM-23309] Admin Console Credit is not Displaying Decimals (#6280) * fix: update calculation to be decimal * fix: update record type property to decimal * tests: add tests to service and update test names --- .../Models/Responses/PaymentMethodResponse.cs | 2 +- src/Core/Billing/Models/PaymentMethod.cs | 2 +- .../Implementations/SubscriberService.cs | 2 +- .../Services/SubscriberServiceTests.cs | 225 ++++++++++++++---- 4 files changed, 183 insertions(+), 48 deletions(-) diff --git a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs index fd248a0a00..a54ac0a876 100644 --- a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs +++ b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs @@ -4,7 +4,7 @@ using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Responses; public record PaymentMethodResponse( - long AccountCredit, + decimal AccountCredit, PaymentSource PaymentSource, string SubscriptionStatus, TaxInformation TaxInformation) diff --git a/src/Core/Billing/Models/PaymentMethod.cs b/src/Core/Billing/Models/PaymentMethod.cs index 14ee79b714..10eab97a8f 100644 --- a/src/Core/Billing/Models/PaymentMethod.cs +++ b/src/Core/Billing/Models/PaymentMethod.cs @@ -6,7 +6,7 @@ using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Models; public record PaymentMethod( - long AccountCredit, + decimal AccountCredit, PaymentSource PaymentSource, string SubscriptionStatus, TaxInformation TaxInformation) diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 84d41f829c..378e84f15a 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -345,7 +345,7 @@ public class SubscriberService( return PaymentMethod.Empty; } - var accountCredit = customer.Balance * -1 / 100; + var accountCredit = customer.Balance * -1 / 100M; var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer); diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 0df8d1bfcc..600f9d9be2 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -329,13 +329,165 @@ public class SubscriberServiceTests #endregion #region GetPaymentMethod + [Theory, BitAutoData] public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException( SutProvider sutProvider) => await Assert.ThrowsAsync(() => sutProvider.Sut.GetPaymentSource(null)); [Theory, BitAutoData] - public async Task GetPaymentMethod_Braintree_NoDefaultPaymentMethod_ReturnsNull( + public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization, + SutProvider sutProvider) + { + // Arrange + // Stripe reports balance in cents as a negative number for credit + const int stripeAccountBalance = -593; // $5.93 credit (negative cents) + const decimal creditAmount = 5.93M; // Same value in dollars + + + var customer = new Customer + { + Balance = stripeAccountBalance, + Subscriptions = new StripeList() + { + Data = + [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] + }, + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethod = new PaymentMethod + { + Type = StripeConstants.PaymentMethodTypes.USBankAccount, + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + } + } + }; + sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") + && options.Expand.Contains("subscriptions") + && options.Expand.Contains("tax_ids"))) + .Returns(customer); + + // Act + var result = await sutProvider.Sut.GetPaymentMethod(organization); + + // Assert + Assert.NotNull(result); + Assert.Equal(creditAmount, result.AccountCredit); + await sutProvider.GetDependency().Received(1).CustomerGetAsync( + organization.GatewayCustomerId, + Arg.Is(options => + options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") && + options.Expand.Contains("subscriptions") && + options.Expand.Contains("tax_ids"))); + + } + + [Theory, BitAutoData] + public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount( + Organization organization, SutProvider sutProvider) + { + // Arrange + const int stripeAccountBalance = 0; + + var customer = new Customer + { + Balance = stripeAccountBalance, + Subscriptions = new StripeList() + { + Data = + [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] + }, + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethod = new PaymentMethod + { + Type = StripeConstants.PaymentMethodTypes.USBankAccount, + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + } + } + }; + sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") + && options.Expand.Contains("subscriptions") + && options.Expand.Contains("tax_ids"))) + .Returns(customer); + + // Act + var result = await sutProvider.Sut.GetPaymentMethod(organization); + + // Assert + Assert.NotNull(result); + Assert.Equal(0, result.AccountCredit); + await sutProvider.GetDependency().Received(1).CustomerGetAsync( + organization.GatewayCustomerId, + Arg.Is(options => + options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") && + options.Expand.Contains("subscriptions") && + options.Expand.Contains("tax_ids"))); + } + + [Theory, BitAutoData] + public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount( + Organization organization, SutProvider sutProvider) + { + // Arrange + const int stripeAccountBalance = 593; // $5.93 charge balance + const decimal accountBalance = -5.93M; // account balance + var customer = new Customer + { + Balance = stripeAccountBalance, + Subscriptions = new StripeList() + { + Data = + [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] + }, + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethod = new PaymentMethod + { + Type = StripeConstants.PaymentMethodTypes.USBankAccount, + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } + } + } + }; + sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") + && options.Expand.Contains("subscriptions") + && options.Expand.Contains("tax_ids"))) + .Returns(customer); + + // Act + var result = await sutProvider.Sut.GetPaymentMethod(organization); + + // Assert + Assert.NotNull(result); + Assert.Equal(accountBalance, result.AccountCredit); + await sutProvider.GetDependency().Received(1).CustomerGetAsync( + organization.GatewayCustomerId, + Arg.Is(options => + options.Expand.Contains("default_source") && + options.Expand.Contains("invoice_settings.default_payment_method") && + options.Expand.Contains("subscriptions") && + options.Expand.Contains("tax_ids"))); + + } + #endregion + + #region GetPaymentSource + + [Theory, BitAutoData] + public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException( + SutProvider sutProvider) => + await Assert.ThrowsAsync(() => sutProvider.Sut.GetPaymentSource(null)); + + [Theory, BitAutoData] + public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull( Provider provider, SutProvider sutProvider) { @@ -372,7 +524,7 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Braintree_PayPalAccount_Succeeds( + public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds( Provider provider, SutProvider sutProvider) { @@ -421,7 +573,7 @@ public class SubscriberServiceTests // TODO: Determine if we need to test Braintree.UsBankAccount [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_BankAccountPaymentMethod_Succeeds( + public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds( Provider provider, SutProvider sutProvider) { @@ -455,7 +607,7 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_CardPaymentMethod_Succeeds( + public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds( Provider provider, SutProvider sutProvider) { @@ -491,43 +643,37 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_SetupIntentForBankAccount_Succeeds( + public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds( Provider provider, SutProvider sutProvider) { - var customer = new Customer - { - Id = provider.GatewayCustomerId - }; + var customer = new Customer { Id = provider.GatewayCustomerId }; sutProvider.GetDependency().CustomerGetAsync(provider.GatewayCustomerId, - Arg.Is( - options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method"))) + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains( + "invoice_settings.default_payment_method"))) .Returns(customer); var setupIntent = new SetupIntent { Id = "setup_intent_id", Status = "requires_action", - NextAction = new SetupIntentNextAction - { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() - }, + NextAction = + new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, PaymentMethod = new PaymentMethod { - UsBankAccount = new PaymentMethodUsBankAccount - { - BankName = "Chase", - Last4 = "9999" - } + UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } } }; sutProvider.GetDependency().Get(provider.Id).Returns(setupIntent.Id); - sutProvider.GetDependency().SetupIntentGet(setupIntent.Id, Arg.Is( - options => options.Expand.Contains("payment_method"))).Returns(setupIntent); + sutProvider.GetDependency().SetupIntentGet(setupIntent.Id, + Arg.Is(options => options.Expand.Contains("payment_method"))).Returns(setupIntent); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); @@ -537,24 +683,19 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_LegacyBankAccount_Succeeds( + public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds( Provider provider, SutProvider sutProvider) { var customer = new Customer { - DefaultSource = new BankAccount - { - Status = "verified", - BankName = "Chase", - Last4 = "9999" - } + DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" } }; sutProvider.GetDependency().CustomerGetAsync(provider.GatewayCustomerId, - Arg.Is( - options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method"))) + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains( + "invoice_settings.default_payment_method"))) .Returns(customer); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); @@ -565,25 +706,19 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_LegacyCard_Succeeds( + public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds( Provider provider, SutProvider sutProvider) { var customer = new Customer { - DefaultSource = new Card - { - Brand = "Visa", - Last4 = "9999", - ExpMonth = 9, - ExpYear = 2028 - } + DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 } }; sutProvider.GetDependency().CustomerGetAsync(provider.GatewayCustomerId, - Arg.Is( - options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method"))) + Arg.Is(options => options.Expand.Contains("default_source") && + options.Expand.Contains( + "invoice_settings.default_payment_method"))) .Returns(customer); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); @@ -594,7 +729,7 @@ public class SubscriberServiceTests } [Theory, BitAutoData] - public async Task GetPaymentMethod_Stripe_LegacySourceCard_Succeeds( + public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds( Provider provider, SutProvider sutProvider) { From 353b596a6d83eb010c0ab50cc35576943192784b Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:59:36 -0500 Subject: [PATCH 203/326] [PM-25390] CORS - Password Change URI (#6287) * enable cors headers for icons program - This is needed now that browsers can hit the change-password-uri path via API call * Add absolute route for change-password-uri --- src/Icons/Controllers/ChangePasswordUriController.cs | 2 +- src/Icons/Startup.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Icons/Controllers/ChangePasswordUriController.cs b/src/Icons/Controllers/ChangePasswordUriController.cs index 3f2bc91cf2..935cda77df 100644 --- a/src/Icons/Controllers/ChangePasswordUriController.cs +++ b/src/Icons/Controllers/ChangePasswordUriController.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Caching.Memory; namespace Bit.Icons.Controllers; -[Route("change-password-uri")] +[Route("~/change-password-uri")] public class ChangePasswordUriController : Controller { private readonly IMemoryCache _memoryCache; diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs index 16bbdef553..2602dd6264 100644 --- a/src/Icons/Startup.cs +++ b/src/Icons/Startup.cs @@ -92,6 +92,9 @@ public class Startup await next(); }); + app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings)) + .AllowAnyMethod().AllowAnyHeader().AllowCredentials()); + app.UseRouting(); app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); } From b7200837c3835c005ec3ae6a45b4a3ffa42d1c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Fri, 5 Sep 2025 19:54:49 +0200 Subject: [PATCH 204/326] [PM-25182] Improve Swagger OperationIDs for Billing (#6238) * Improve Swagger OperationIDs for Billing * Fix typo --- .../OrganizationSponsorshipsController.cs | 20 +++++++++++++++++-- ...elfHostedOrganizationLicensesController.cs | 4 ++-- ...ostedOrganizationSponsorshipsController.cs | 8 +++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 2d05595b2d..8c202752de 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -208,7 +208,6 @@ public class OrganizationSponsorshipsController : Controller [Authorize("Application")] [HttpDelete("{sponsoringOrganizationId}")] - [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RevokeSponsorship(Guid sponsoringOrganizationId) { @@ -225,6 +224,15 @@ public class OrganizationSponsorshipsController : Controller await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + [Authorize("Application")] + [HttpPost("{sponsoringOrganizationId}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE /{sponsoringOrganizationId} instead.")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PostRevokeSponsorship(Guid sponsoringOrganizationId) + { + await RevokeSponsorship(sponsoringOrganizationId); + } + [Authorize("Application")] [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] [SelfHosted(NotSelfHostedOnly = true)] @@ -241,7 +249,6 @@ public class OrganizationSponsorshipsController : Controller [Authorize("Application")] [HttpDelete("sponsored/{sponsoredOrgId}")] - [HttpPost("sponsored/{sponsoredOrgId}/remove")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RemoveSponsorship(Guid sponsoredOrgId) { @@ -257,6 +264,15 @@ public class OrganizationSponsorshipsController : Controller await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship); } + [Authorize("Application")] + [HttpPost("sponsored/{sponsoredOrgId}/remove")] + [Obsolete("This endpoint is deprecated. Use DELETE /sponsored/{sponsoredOrgId} instead.")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task PostRemoveSponsorship(Guid sponsoredOrgId) + { + await RemoveSponsorship(sponsoredOrgId); + } + [HttpGet("{sponsoringOrgId}/sync-status")] public async Task GetSyncStatus(Guid sponsoringOrgId) { diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs index b4eecdba0f..147f2d52ee 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -53,7 +53,7 @@ public class SelfHostedOrganizationLicensesController : Controller } [HttpPost("")] - public async Task PostLicenseAsync(OrganizationCreateLicenseRequestModel model) + public async Task CreateLicenseAsync(OrganizationCreateLicenseRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -74,7 +74,7 @@ public class SelfHostedOrganizationLicensesController : Controller } [HttpPost("{id}")] - public async Task PostLicenseAsync(string id, LicenseRequestModel model) + public async Task UpdateLicenseAsync(string id, LicenseRequestModel model) { var orgIdGuid = new Guid(id); if (!await _currentContext.OrganizationOwner(orgIdGuid)) diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index de41a4cf10..198438201c 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -79,7 +79,6 @@ public class SelfHostedOrganizationSponsorshipsController : Controller } [HttpDelete("{sponsoringOrgId}")] - [HttpPost("{sponsoringOrgId}/delete")] public async Task RevokeSponsorship(Guid sponsoringOrgId) { var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); @@ -95,6 +94,13 @@ public class SelfHostedOrganizationSponsorshipsController : Controller await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + [HttpPost("{sponsoringOrgId}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE /{sponsoringOrgId} instead.")] + public async Task PostRevokeSponsorship(Guid sponsoringOrgId) + { + await RevokeSponsorship(sponsoringOrgId); + } + [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName) { From 2a01c804af1d4dd6e64ea08de11c05978c3e7ad9 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 8 Sep 2025 10:49:00 +0000 Subject: [PATCH 205/326] Bumped version to 2025.9.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3af05be0f1..66fb49300c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.8.1 + 2025.9.0 Bit.$(MSBuildProjectName) enable From 7e50a46d3b315de379dfdd0c353f7f7cfd5a6c11 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:12:43 -0400 Subject: [PATCH 206/326] chore(feature-flag): Remove persist-popup-view feature flag --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 57798204ea..69003ee253 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -199,7 +199,6 @@ public static class FeatureFlagKeys public const string SendAccess = "pm-19394-send-access-control"; /* Platform Team */ - public const string PersistPopupView = "persist-popup-view"; public const string IpcChannelFramework = "ipc-channel-framework"; public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked"; public const string PushNotificationsWhenInactive = "pm-25130-receive-push-notifications-for-inactive-users"; From 0fbbb6a984a84463e2912178e815a6b07528c9df Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:54:43 -0400 Subject: [PATCH 207/326] Event integration updates and cleanups (#6288) * Event integration updates and cleanups * Fix empty message on ArgumentException * Adjust exception message Co-authored-by: Matt Bishop --------- Co-authored-by: Matt Bishop --- .../Services/EventLoggingListenerService.cs | 4 +- .../Services/IEventMessageHandler.cs | 5 +- .../Services/IIntegrationHandler.cs | 55 +++++++++++++++++-- .../EventIntegrations/README.md | 12 +++- .../WebhookIntegrationHandler.cs | 46 ++-------------- .../Models/OrganizationIntegration.cs | 7 +-- .../OrganizationIntegrationConfiguration.cs | 7 +-- .../WebhookIntegrationHandlerTests.cs | 4 ++ 8 files changed, 76 insertions(+), 64 deletions(-) diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs index 53ff3d4d0a..84a862ce94 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs @@ -28,12 +28,12 @@ public abstract class EventLoggingListenerService : BackgroundService if (root.ValueKind == JsonValueKind.Array) { var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); + await _handler.HandleManyEventsAsync(eventMessages ?? throw new JsonException("Deserialize returned null")); } else if (root.ValueKind == JsonValueKind.Object) { var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); + await _handler.HandleEventAsync(eventMessage ?? throw new JsonException("Deserialize returned null")); } else { diff --git a/src/Core/AdminConsole/Services/IEventMessageHandler.cs b/src/Core/AdminConsole/Services/IEventMessageHandler.cs index fcffb56c65..83c5e33ecb 100644 --- a/src/Core/AdminConsole/Services/IEventMessageHandler.cs +++ b/src/Core/AdminConsole/Services/IEventMessageHandler.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs index 9a3edac9ec..bb10dc01b9 100644 --- a/src/Core/AdminConsole/Services/IIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -1,6 +1,5 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - +using System.Globalization; +using System.Net; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; @@ -20,8 +19,56 @@ public abstract class IntegrationHandlerBase : IIntegrationHandler public async Task HandleAsync(string json) { var message = IntegrationMessage.FromJson(json); - return await HandleAsync(message); + return await HandleAsync(message ?? throw new ArgumentException("IntegrationMessage was null when created from the provided JSON")); } public abstract Task HandleAsync(IntegrationMessage message); + + protected IntegrationHandlerResult ResultFromHttpResponse( + HttpResponseMessage response, + IntegrationMessage message, + TimeProvider timeProvider) + { + var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); + + if (response.IsSuccessStatusCode) return result; + + switch (response.StatusCode) + { + case HttpStatusCode.TooManyRequests: + case HttpStatusCode.RequestTimeout: + case HttpStatusCode.InternalServerError: + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + result.Retryable = true; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; + + if (response.Headers.TryGetValues("Retry-After", out var values)) + { + var value = values.FirstOrDefault(); + if (int.TryParse(value, out var seconds)) + { + // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. + result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime; + } + else if (DateTimeOffset.TryParseExact(value, + "r", // "r" is the round-trip format: RFC1123 + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var retryDate)) + { + // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date. + result.DelayUntilDate = retryDate.UtcDateTime; + } + } + break; + default: + result.Retryable = false; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; + break; + } + + return result; + } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index 83b59cdec1..4092cc20ad 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -418,13 +418,21 @@ dependencies and integrations. For instance, `SlackIntegrationHandler` needs a ` `AddEventIntegrationServices` has a call to `AddSlackService`. Same thing for webhooks when it comes to defining a custom HttpClient by name. -1. In `AddEventIntegrationServices` create the listener configuration: +In `AddEventIntegrationServices`: + +1. Create the singleton for the handler: + +``` csharp + services.TryAddSingleton, ExampleIntegrationHandler>(); +``` + +2. Create the listener configuration: ``` csharp var exampleConfiguration = new ExampleListenerConfiguration(globalSettings); ``` -2. Add the integration to both the RabbitMQ and ASB specific declarations: +3. Add the integration to both the RabbitMQ and ASB specific declarations: ``` csharp services.AddRabbitMqIntegration(exampleConfiguration); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index 99cad65efa..e0c2b66a90 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -1,7 +1,5 @@ #nullable enable -using System.Globalization; -using System.Net; using System.Net.Http.Headers; using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; @@ -17,7 +15,8 @@ public class WebhookIntegrationHandler( public const string HttpClientName = "WebhookIntegrationHandlerHttpClient"; - public override async Task HandleAsync(IntegrationMessage message) + public override async Task HandleAsync( + IntegrationMessage message) { var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri); request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); @@ -28,45 +27,8 @@ public class WebhookIntegrationHandler( parameter: message.Configuration.Token ); } + var response = await _httpClient.SendAsync(request); - var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); - - switch (response.StatusCode) - { - case HttpStatusCode.TooManyRequests: - case HttpStatusCode.RequestTimeout: - case HttpStatusCode.InternalServerError: - case HttpStatusCode.BadGateway: - case HttpStatusCode.ServiceUnavailable: - case HttpStatusCode.GatewayTimeout: - result.Retryable = true; - result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; - - if (response.Headers.TryGetValues("Retry-After", out var values)) - { - var value = values.FirstOrDefault(); - if (int.TryParse(value, out var seconds)) - { - // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. - result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime; - } - else if (DateTimeOffset.TryParseExact(value, - "r", // "r" is the round-trip format: RFC1123 - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, - out var retryDate)) - { - // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date. - result.DelayUntilDate = retryDate.UtcDateTime; - } - } - break; - default: - result.Retryable = false; - result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; - break; - } - - return result; + return ResultFromHttpResponse(response, message, timeProvider); } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs index 5e5f7d4802..0f47d5947b 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegration.cs @@ -1,13 +1,10 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using AutoMapper; +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration { - public virtual Organization Organization { get; set; } + public virtual required Organization Organization { get; set; } } public class OrganizationIntegrationMapperProfile : Profile diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs index 52b8783fcf..21b282f767 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Models/OrganizationIntegrationConfiguration.cs @@ -1,13 +1,10 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using AutoMapper; +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration { - public virtual OrganizationIntegration OrganizationIntegration { get; set; } + public virtual required OrganizationIntegration OrganizationIntegration { get; set; } } public class OrganizationIntegrationConfigurationMapperProfile : Profile diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index bf4283243c..53a3598d47 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -51,6 +51,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Success); Assert.Equal(result.Message, message); + Assert.Empty(result.FailureReason); sutProvider.GetDependency().Received(1).CreateClient( Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) @@ -59,6 +60,7 @@ public class WebhookIntegrationHandlerTests Assert.Single(_handler.CapturedRequests); var request = _handler.CapturedRequests[0]; Assert.NotNull(request); + Assert.NotNull(request.Content); var returned = await request.Content.ReadAsStringAsync(); Assert.Equal(HttpMethod.Post, request.Method); @@ -77,6 +79,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Success); Assert.Equal(result.Message, message); + Assert.Empty(result.FailureReason); sutProvider.GetDependency().Received(1).CreateClient( Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) @@ -85,6 +88,7 @@ public class WebhookIntegrationHandlerTests Assert.Single(_handler.CapturedRequests); var request = _handler.CapturedRequests[0]; Assert.NotNull(request); + Assert.NotNull(request.Content); var returned = await request.Content.ReadAsStringAsync(); Assert.Equal(HttpMethod.Post, request.Method); From 39ad02041870dc67150937b79043f110d44d60fd Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:23:08 -0700 Subject: [PATCH 208/326] [PM-22219] - [Vault] [Server] Exclude items in default collections from Admin Console (#5992) * add GetAllOrganizationCiphersExcludingDefaultUserCollections * add sproc * update sproc and feature flag name * add sproc. update tests * rename sproc * rename sproc * use single sproc * revert change * remove unused code. update sproc * remove joins from proc * update migration filename * fix syntax * fix indentation * remove unnecessary feature flag and go statements. clean up code * update sproc, view, and index * update sproc * update index * update timestamp * update filename. update sproc to match EF filter * match only enabled organizations. make index creation idempotent * update file timestamp * update timestamp * use square brackets * add square brackets * formatting fixes * rename view * remove index --- .../Vault/Controllers/CiphersController.cs | 12 +++- .../Queries/IOrganizationCiphersQuery.cs | 6 ++ .../Vault/Queries/OrganizationCiphersQuery.cs | 6 ++ .../Vault/Repositories/ICipherRepository.cs | 6 ++ .../Vault/Repositories/CipherRepository.cs | 42 +++++++++++ .../Vault/Repositories/CipherRepository.cs | 50 ++++++++++++++ ...anizationIdExcludingDefaultCollections.sql | 39 +++++++++++ src/Sql/dbo/Vault/Tables/Cipher.sql | 1 + ...ganizationCipherDetailsCollectionsView.sql | 28 ++++++++ .../Queries/OrganizationCiphersQueryTests.cs | 43 ++++++++++++ ...tionDetailsExcludingDefaultCollections.sql | 69 +++++++++++++++++++ 11 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections.sql create mode 100644 src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql create mode 100644 util/Migrator/DbScripts/2025-09-03_00_CipherOrganizationDetailsExcludingDefaultCollections.sql diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 761a5a3726..84e0488e5a 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -48,6 +48,7 @@ public class CiphersController : Controller private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly IApplicationCacheService _applicationCacheService; private readonly ICollectionRepository _collectionRepository; + private readonly IFeatureService _featureService; public CiphersController( ICipherRepository cipherRepository, @@ -61,7 +62,8 @@ public class CiphersController : Controller GlobalSettings globalSettings, IOrganizationCiphersQuery organizationCiphersQuery, IApplicationCacheService applicationCacheService, - ICollectionRepository collectionRepository) + ICollectionRepository collectionRepository, + IFeatureService featureService) { _cipherRepository = cipherRepository; _collectionCipherRepository = collectionCipherRepository; @@ -75,6 +77,7 @@ public class CiphersController : Controller _organizationCiphersQuery = organizationCiphersQuery; _applicationCacheService = applicationCacheService; _collectionRepository = collectionRepository; + _featureService = featureService; } [HttpGet("{id}")] @@ -314,8 +317,11 @@ public class CiphersController : Controller { throw new NotFoundException(); } - - var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); + var allOrganizationCiphers = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + ? + await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId) + : + await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); var allOrganizationCipherResponses = allOrganizationCiphers.Select(c => diff --git a/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs b/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs index 1756cad3c7..44a56eac48 100644 --- a/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs +++ b/src/Core/Vault/Queries/IOrganizationCiphersQuery.cs @@ -37,4 +37,10 @@ public interface IOrganizationCiphersQuery /// public Task> GetOrganizationCiphersByCollectionIds( Guid organizationId, IEnumerable collectionIds); + + /// + /// Returns all organization ciphers except those in default user collections. + /// + public Task> + GetAllOrganizationCiphersExcludingDefaultUserCollections(Guid organizationId); } diff --git a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs index deed121216..945fdb7e3c 100644 --- a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs +++ b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs @@ -61,4 +61,10 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery var allOrganizationCiphers = await GetAllOrganizationCiphers(organizationId); return allOrganizationCiphers.Where(c => c.CollectionIds.Intersect(managedCollectionIds).Any()); } + + public async Task> + GetAllOrganizationCiphersExcludingDefaultUserCollections(Guid orgId) + { + return (await _cipherRepository.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(orgId)).ToList(); + } } diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 60b6e21f1d..e442477921 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -84,6 +84,12 @@ public interface ICipherRepository : IRepository /// A list of ciphers with updated data UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable ciphers); + + /// + /// Returns all ciphers belonging to the organization excluding those with default collections + /// + Task> + GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId); /// /// /// This version uses the bulk resource creation service to create the temp table. diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 8c1f04affc..08593191f1 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -7,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; using Bit.Core.Tools.Entities; +using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; @@ -867,6 +868,47 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task> + GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid orgId) + { + await using var connection = new SqlConnection(ConnectionString); + + var dict = new Dictionary(); + var tempCollections = new Dictionary>(); + + await connection.QueryAsync( + $"[{Schema}].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections]", + (cipher, cc) => + { + if (!dict.TryGetValue(cipher.Id, out var details)) + { + details = new CipherOrganizationDetailsWithCollections(cipher, /*dummy*/null); + dict.Add(cipher.Id, details); + tempCollections[cipher.Id] = new List(); + } + + if (cc?.CollectionId != null) + { + tempCollections[cipher.Id].AddIfNotExists(cc.CollectionId); + } + + return details; + }, + new { OrganizationId = orgId }, + splitOn: "CollectionId", + commandType: CommandType.StoredProcedure + ); + + // now assign each List back to the array property in one shot + foreach (var kv in dict) + { + kv.Value.CollectionIds = tempCollections[kv.Key].ToArray(); + } + + return dict.Values.ToList(); + } + + private DataTable BuildCiphersTable(SqlBulkCopy bulkCopy, IEnumerable ciphers) { var c = ciphers.FirstOrDefault(); diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index d595fe7cfe..1a137c5f4b 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; +using Bit.Core.Enums; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Utilities; using Bit.Core.Vault.Enums; @@ -1001,6 +1002,55 @@ public class CipherRepository : Repository> + GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(Guid organizationId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var defaultTypeInt = (int)CollectionType.DefaultUserCollection; + + // filter out any cipher that belongs *only* to default collections + // i.e. keep ciphers with no collections, or with ≥1 non-default collection + var query = from c in dbContext.Ciphers.AsNoTracking() + where c.UserId == null + && c.OrganizationId == organizationId + && c.Organization.Enabled + && ( + c.CollectionCiphers.Count() == 0 + || c.CollectionCiphers.Any(cc => (int)cc.Collection.Type != defaultTypeInt) + ) + select new CipherOrganizationDetailsWithCollections( + new CipherOrganizationDetails + { + Id = c.Id, + UserId = c.UserId, + OrganizationId = c.OrganizationId, + Type = c.Type, + Data = c.Data, + Favorites = c.Favorites, + Folders = c.Folders, + Attachments = c.Attachments, + CreationDate = c.CreationDate, + RevisionDate = c.RevisionDate, + DeletedDate = c.DeletedDate, + Reprompt = c.Reprompt, + Key = c.Key, + OrganizationUseTotp = c.Organization.UseTotp + }, + new Dictionary>() + ) + { + CollectionIds = c.CollectionCiphers + .Where(cc => (int)cc.Collection.Type != defaultTypeInt) + .Select(cc => cc.CollectionId) + .ToArray() + }; + + var result = await query.ToListAsync(); + return result; + } + /// /// /// EF does not use the bulk resource creation service, so we need to use the regular update method. diff --git a/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections.sql b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections.sql new file mode 100644 index 0000000000..c678386f8a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections.sql @@ -0,0 +1,39 @@ + -- Stored procedure that filters out ciphers that ONLY belong to default collections +CREATE PROCEDURE + [dbo].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections] + @OrganizationId UNIQUEIDENTIFIER + AS + BEGIN + SET NOCOUNT ON; + + WITH [NonDefaultCiphers] AS ( + SELECT DISTINCT [Id] + FROM [dbo].[OrganizationCipherDetailsCollectionsView] + WHERE [OrganizationId] = @OrganizationId + AND ([CollectionId] IS NULL + OR [CollectionType] <> 1) + ) + + SELECT + V.[Id], + V.[UserId], + V.[OrganizationId], + V.[Type], + V.[Data], + V.[Favorites], + V.[Folders], + V.[Attachments], + V.[CreationDate], + V.[RevisionDate], + V.[DeletedDate], + V.[Reprompt], + V.[Key], + V.[OrganizationUseTotp], + V.[CollectionId] -- For Dapper splitOn parameter + FROM [dbo].[OrganizationCipherDetailsCollectionsView] V + INNER JOIN [NonDefaultCiphers] NDC ON V.[Id] = NDC.[Id] + WHERE V.[OrganizationId] = @OrganizationId + AND (V.[CollectionId] IS NULL OR V.[CollectionType] <> 1) + ORDER BY V.[RevisionDate] DESC; + END; + GO \ No newline at end of file diff --git a/src/Sql/dbo/Vault/Tables/Cipher.sql b/src/Sql/dbo/Vault/Tables/Cipher.sql index 5ecff19e70..38dd47d21f 100644 --- a/src/Sql/dbo/Vault/Tables/Cipher.sql +++ b/src/Sql/dbo/Vault/Tables/Cipher.sql @@ -34,3 +34,4 @@ GO CREATE NONCLUSTERED INDEX [IX_Cipher_DeletedDate] ON [dbo].[Cipher]([DeletedDate] ASC); +GO diff --git a/src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql b/src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql new file mode 100644 index 0000000000..66bb38fe10 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql @@ -0,0 +1,28 @@ +CREATE VIEW [dbo].[OrganizationCipherDetailsCollectionsView] +AS + SELECT + C.[Id], + C.[UserId], + C.[OrganizationId], + C.[Type], + C.[Data], + C.[Attachments], + C.[Favorites], + C.[Folders], + C.[CreationDate], + C.[RevisionDate], + C.[DeletedDate], + C.[Reprompt], + C.[Key], + CASE + WHEN O.[UseTotp] = 1 THEN 1 + ELSE 0 + END AS [OrganizationUseTotp], + CC.[CollectionId], + COL.[Type] AS [CollectionType] + FROM [dbo].[Cipher] C + INNER JOIN [dbo].[Organization] O ON C.[OrganizationId] = O.[Id] + LEFT JOIN [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] + LEFT JOIN [dbo].[Collection] COL ON CC.[CollectionId] = COL.[Id] + WHERE C.[UserId] IS NULL -- Organization ciphers only + AND O.[Enabled] = 1; -- Only enabled organizations diff --git a/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs b/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs index 01539fe7d7..0d7443354e 100644 --- a/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs +++ b/test/Core.Test/Vault/Queries/OrganizationCiphersQueryTests.cs @@ -89,4 +89,47 @@ public class OrganizationCiphersQueryTests c.CollectionIds.Any(cId => cId == targetCollectionId) && c.CollectionIds.Any(cId => cId == otherCollectionId)); } + + + [Theory, BitAutoData] + public async Task GetAllOrganizationCiphersExcludingDefaultUserCollections_DelegatesToRepository( + Guid organizationId, + SutProvider sutProvider) + { + var item1 = new CipherOrganizationDetailsWithCollections( + new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId }, + new Dictionary>()); + var item2 = new CipherOrganizationDetailsWithCollections( + new CipherOrganizationDetails { Id = Guid.NewGuid(), OrganizationId = organizationId }, + new Dictionary>()); + + var repo = sutProvider.GetDependency(); + repo.GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId) + .Returns(Task.FromResult>( + new[] { item1, item2 })); + + var actual = (await sutProvider.Sut + .GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId)) + .ToList(); + + Assert.Equal(2, actual.Count); + Assert.Same(item1, actual[0]); + Assert.Same(item2, actual[1]); + + // and we indeed called the repo once + await repo.Received(1) + .GetManyCipherOrganizationDetailsExcludingDefaultCollectionsAsync(organizationId); + } + + private CipherOrganizationDetailsWithCollections MakeWith( + CipherOrganizationDetails baseCipher, + params Guid[] cols) + { + var dict = cols + .Select(cid => new CollectionCipher { CipherId = baseCipher.Id, CollectionId = cid }) + .GroupBy(cc => cc.CipherId) + .ToDictionary(g => g.Key, g => g); + + return new CipherOrganizationDetailsWithCollections(baseCipher, dict); + } } diff --git a/util/Migrator/DbScripts/2025-09-03_00_CipherOrganizationDetailsExcludingDefaultCollections.sql b/util/Migrator/DbScripts/2025-09-03_00_CipherOrganizationDetailsExcludingDefaultCollections.sql new file mode 100644 index 0000000000..a7dfa2f7d7 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-03_00_CipherOrganizationDetailsExcludingDefaultCollections.sql @@ -0,0 +1,69 @@ +-- View that provides organization cipher details with their collection associations +CREATE OR ALTER VIEW [dbo].[OrganizationCipherDetailsCollectionsView] +AS + SELECT + C.[Id], + C.[UserId], + C.[OrganizationId], + C.[Type], + C.[Data], + C.[Attachments], + C.[Favorites], + C.[Folders], + C.[CreationDate], + C.[RevisionDate], + C.[DeletedDate], + C.[Reprompt], + C.[Key], + CASE + WHEN O.[UseTotp] = 1 THEN 1 + ELSE 0 + END AS [OrganizationUseTotp], + CC.[CollectionId], + COL.[Type] AS [CollectionType] + FROM [dbo].[Cipher] C + INNER JOIN [dbo].[Organization] O ON C.[OrganizationId] = O.[Id] + LEFT JOIN [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] + LEFT JOIN [dbo].[Collection] COL ON CC.[CollectionId] = COL.[Id] + WHERE C.[UserId] IS NULL -- Organization ciphers only + AND O.[Enabled] = 1; -- Only enabled organizations +GO + + -- Stored procedure that filters out ciphers that ONLY belong to default collections +CREATE OR ALTER PROCEDURE + [dbo].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections] + @OrganizationId UNIQUEIDENTIFIER + AS + BEGIN + SET NOCOUNT ON; + + WITH [NonDefaultCiphers] AS ( + SELECT DISTINCT [Id] + FROM [dbo].[OrganizationCipherDetailsCollectionsView] + WHERE [OrganizationId] = @OrganizationId + AND ([CollectionId] IS NULL OR [CollectionType] <> 1) + ) + + SELECT + V.[Id], + V.[UserId], + V.[OrganizationId], + V.[Type], + V.[Data], + V.[Favorites], + V.[Folders], + V.[Attachments], + V.[CreationDate], + V.[RevisionDate], + V.[DeletedDate], + V.[Reprompt], + V.[Key], + V.[OrganizationUseTotp], + V.[CollectionId] -- For Dapper splitOn parameter + FROM [dbo].[OrganizationCipherDetailsCollectionsView] V + INNER JOIN [NonDefaultCiphers] NDC ON V.[Id] = NDC.[Id] + WHERE V.[OrganizationId] = @OrganizationId + AND (V.[CollectionId] IS NULL OR V.[CollectionType] <> 1) + ORDER BY V.[RevisionDate] DESC; + END; +GO From 747e212b1ba374e4c1be9cfe255bb40c002d0ceb Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:39:59 -0400 Subject: [PATCH 209/326] Add Datadog integration (#6289) * Event integration updates and cleanups * Add Datadog integration * Update README to include link to Datadog PR * Move doc update into the Datadog PR; Fix empty message on ArgumentException * Adjust exception message Co-authored-by: Matt Bishop * Removed unnecessary nullable enable; Moved Docs link to PR into this PR * Remove unnecessary nullable enable calls --------- Co-authored-by: Matt Bishop --- dev/servicebusemulator_config.json | 17 ++ ...ionIntegrationConfigurationRequestModel.cs | 8 +- .../OrgnizationIntegrationRequestModel.cs | 10 +- .../AdminConsole/Enums/IntegrationType.cs | 5 +- .../EventIntegrations/DatadogIntegration.cs | 3 + .../DatadogIntegrationConfigurationDetails.cs | 3 + .../DatadogListenerConfiguration.cs | 38 +++++ .../DatadogIntegrationHandler.cs | 25 +++ .../EventIntegrations/README.md | 3 +- src/Core/Settings/GlobalSettings.cs | 5 + .../Utilities/ServiceCollectionExtensions.cs | 5 + ...tegrationConfigurationRequestModelTests.cs | 26 +++ ...rganizationIntegrationRequestModelTests.cs | 48 ++++++ .../DatadogIntegrationHandlerTests.cs | 157 ++++++++++++++++++ 14 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs create mode 100644 src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs create mode 100644 test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index dcf48b7a8c..294efc1897 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -34,6 +34,9 @@ }, { "Name": "events-hec-subscription" + }, + { + "Name": "events-datadog-subscription" } ] }, @@ -81,6 +84,20 @@ } } ] + }, + { + "Name": "integration-datadog-subscription", + "Rules": [ + { + "Name": "datadog-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "datadog" + } + } + } + ] } ] } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index 17e116b8d1..7d1efe2315 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; @@ -36,6 +34,10 @@ public class OrganizationIntegrationConfigurationRequestModel return !string.IsNullOrWhiteSpace(Template) && Configuration is null && IsFiltersValid(); + case IntegrationType.Datadog: + 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 edae0719e3..5fa2e86a90 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs @@ -4,8 +4,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; -#nullable enable - namespace Bit.Api.AdminConsole.Models.Request.Organizations; public class OrganizationIntegrationRequestModel : IValidatableObject @@ -60,6 +58,14 @@ public class OrganizationIntegrationRequestModel : IValidatableObject new[] { nameof(Configuration) }); } break; + case IntegrationType.Datadog: + if (!IsIntegrationValid()) + { + yield return new ValidationResult( + "Datadog integrations must include valid configuration.", + new[] { nameof(Configuration) }); + } + break; default: yield return new ValidationResult( $"Integration type '{Type}' is not recognized.", diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs index 58e55193dc..34edc71fbe 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -6,7 +6,8 @@ public enum IntegrationType : int Scim = 2, Slack = 3, Webhook = 4, - Hec = 5 + Hec = 5, + Datadog = 6 } public static class IntegrationTypeExtensions @@ -21,6 +22,8 @@ public static class IntegrationTypeExtensions return "webhook"; case IntegrationType.Hec: return "hec"; + case IntegrationType.Datadog: + return "datadog"; default: throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}"); } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs new file mode 100644 index 0000000000..8785a74896 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegration.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record DatadogIntegration(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs new file mode 100644 index 0000000000..07aafa4bd8 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogIntegrationConfigurationDetails.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs new file mode 100644 index 0000000000..1c74826791 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/DatadogListenerConfiguration.cs @@ -0,0 +1,38 @@ +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +public class DatadogListenerConfiguration(GlobalSettings globalSettings) + : ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration +{ + public IntegrationType IntegrationType + { + get => IntegrationType.Datadog; + } + + public string EventQueueName + { + get => _globalSettings.EventLogging.RabbitMq.DatadogEventsQueueName; + } + + public string IntegrationQueueName + { + get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationQueueName; + } + + public string IntegrationRetryQueueName + { + get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationRetryQueueName; + } + + public string EventSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.DatadogEventSubscriptionName; + } + + public string IntegrationSubscriptionName + { + get => _globalSettings.EventLogging.AzureServiceBus.DatadogIntegrationSubscriptionName; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs new file mode 100644 index 0000000000..45bb5b6d7d --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/DatadogIntegrationHandler.cs @@ -0,0 +1,25 @@ +using System.Text; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; + +namespace Bit.Core.Services; + +public class DatadogIntegrationHandler( + IHttpClientFactory httpClientFactory, + TimeProvider timeProvider) + : IntegrationHandlerBase +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + + public const string HttpClientName = "DatadogIntegrationHandlerHttpClient"; + + public override async Task HandleAsync(IntegrationMessage message) + { + var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri); + request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); + request.Headers.Add("DD-API-KEY", message.Configuration.ApiKey); + + var response = await _httpClient.SendAsync(request); + + return ResultFromHttpResponse(response, message, timeProvider); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md index 4092cc20ad..de7ce3f7fd 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/README.md @@ -323,7 +323,8 @@ A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the bac # Building a new integration These are all the pieces required in the process of building out a new integration. For -clarity in naming, these assume a new integration called "Example". +clarity in naming, these assume a new integration called "Example". To see a complete example +in context, view [the PR for adding the Datadog integration](https://github.com/bitwarden/server/pull/6289). ## IntegrationType diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index d6e18a4c81..638e1477c1 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -304,6 +304,8 @@ public class GlobalSettings : IGlobalSettings public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription"; public virtual string HecEventSubscriptionName { get; set; } = "events-hec-subscription"; 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 string ConnectionString { @@ -345,6 +347,9 @@ public class GlobalSettings : IGlobalSettings public virtual string HecEventsQueueName { get; set; } = "events-hec-queue"; public virtual string HecIntegrationQueueName { get; set; } = "integration-hec-queue"; public virtual string HecIntegrationRetryQueueName { get; set; } = "integration-hec-retry-queue"; + 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 string HostName { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 4f0d0d4397..592f7c84c3 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -881,15 +881,18 @@ public static class ServiceCollectionExtensions services.AddSlackService(globalSettings); services.TryAddSingleton(TimeProvider.System); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddHttpClient(DatadogIntegrationHandler.HttpClientName); // Add integration handlers services.TryAddSingleton, SlackIntegrationHandler>(); services.TryAddSingleton, WebhookIntegrationHandler>(); + services.TryAddSingleton, DatadogIntegrationHandler>(); 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); if (IsRabbitMqEnabled(globalSettings)) { @@ -906,6 +909,7 @@ public static class ServiceCollectionExtensions services.AddRabbitMqIntegration(slackConfiguration); services.AddRabbitMqIntegration(webhookConfiguration); services.AddRabbitMqIntegration(hecConfiguration); + services.AddRabbitMqIntegration(datadogConfiguration); } if (IsAzureServiceBusEnabled(globalSettings)) @@ -923,6 +927,7 @@ public static class ServiceCollectionExtensions services.AddAzureServiceBusIntegration(slackConfiguration); services.AddAzureServiceBusIntegration(webhookConfiguration); services.AddAzureServiceBusIntegration(hecConfiguration); + services.AddAzureServiceBusIntegration(datadogConfiguration); } return services; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 6af5b8039b..74fe75a9d7 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -62,6 +62,32 @@ 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)); + } + [Theory] [InlineData(data: null)] [InlineData(data: "")] diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs index 147564dd94..9565a76822 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs @@ -147,6 +147,54 @@ public class OrganizationIntegrationRequestModelTests Assert.Empty(results); } + [Fact] + public void Validate_Datadog_WithNullConfiguration_ReturnsError() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Datadog, + Configuration = null + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Single(results); + Assert.Contains(nameof(model.Configuration), results[0].MemberNames); + Assert.Contains("must include valid configuration", results[0].ErrorMessage); + } + + [Fact] + public void Validate_Datadog_WithInvalidConfiguration_ReturnsError() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Datadog, + Configuration = "Not valid" + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Single(results); + Assert.Contains(nameof(model.Configuration), results[0].MemberNames); + Assert.Contains("must include valid configuration", results[0].ErrorMessage); + } + + [Fact] + public void Validate_Datadog_WithValidConfiguration_ReturnsNoErrors() + { + var model = new OrganizationIntegrationRequestModel + { + Type = IntegrationType.Datadog, + Configuration = JsonSerializer.Serialize( + new DatadogIntegration(ApiKey: "API1234", Uri: new Uri("http://localhost")) + ) + }; + + var results = model.Validate(new ValidationContext(model)).ToList(); + + Assert.Empty(results); + } + [Fact] public void Validate_UnknownIntegrationType_ReturnsUnrecognizedError() { diff --git a/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs new file mode 100644 index 0000000000..5f0a9915bf --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/DatadogIntegrationHandlerTests.cs @@ -0,0 +1,157 @@ +#nullable enable + +using System.Net; +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 Bit.Test.Common.MockedHttpClient; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class DatadogIntegrationHandlerTests +{ + private readonly MockedHttpMessageHandler _handler; + private readonly HttpClient _httpClient; + private const string _apiKey = "AUTH_TOKEN"; + private static readonly Uri _datadogUri = new Uri("https://localhost"); + + public DatadogIntegrationHandlerTests() + { + _handler = new MockedHttpMessageHandler(); + _handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent(new StringContent("testtest")); + _httpClient = _handler.ToHttpClient(); + } + + private SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(DatadogIntegrationHandler.HttpClientName).Returns(_httpClient); + + return new SutProvider() + .SetDependency(clientFactory) + .WithFakeTimeProvider() + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + Assert.Empty(result.FailureReason); + + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(DatadogIntegrationHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + Assert.NotNull(request.Content); + var returned = await request.Content.ReadAsStringAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_apiKey, request.Headers.GetValues("DD-API-KEY").Single()); + Assert.Equal(_datadogUri, request.RequestUri); + AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsDelayUntilDate(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc); + var retryAfter = now.AddSeconds(60); + + sutProvider.GetDependency().SetUtcNow(now); + message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", "60") + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.Equal(retryAfter, result.DelayUntilDate.Value); + Assert.Equal("Too Many Requests", result.FailureReason); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsDelayUntilDate(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc); + var retryAfter = now.AddSeconds(60); + message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", retryAfter.ToString("r")) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.Equal(retryAfter, result.DelayUntilDate.Value); + Assert.Equal("Too Many Requests", result.FailureReason); + } + + [Theory, BitAutoData] + public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.InternalServerError) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.False(result.DelayUntilDate.HasValue); + Assert.Equal("Internal Server Error", result.FailureReason); + } + + [Theory, BitAutoData] + public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new DatadogIntegrationConfigurationDetails(ApiKey: _apiKey, Uri: _datadogUri); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TemporaryRedirect) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + Assert.Null(result.DelayUntilDate); + Assert.Equal("Temporary Redirect", result.FailureReason); + } +} From cb0d5a5ba60b5da47bf14b4bbf6d87313f75bc0a Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 8 Sep 2025 19:45:06 +0000 Subject: [PATCH 210/326] Bumped version to 2025.9.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 66fb49300c..9038c8d95d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.9.0 + 2025.9.1 Bit.$(MSBuildProjectName) enable From 226f274a7237e4c2811fd82ecb409c650f525d3d Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Mon, 8 Sep 2025 15:06:13 -0500 Subject: [PATCH 211/326] Organization report tables, repos, services, and endpoints (#6158) * PM-23754 initial commit * pm-23754 fixing controller tests * pm-23754 adding commands and queries * pm-23754 adding endpoints, command/queries, repositories, and sql migrations * pm-23754 add new sql scripts * PM-23754 adding sql scripts * pm-23754 * PM-23754 fixing migration script * PM-23754 fixing migration script again * PM-23754 fixing migration script validation * PM-23754 fixing db validation script issue * PM-23754 fixing endpoint and db validation * PM-23754 fixing unit tests * PM-23754 fixing implementation based on comments and tests * PM-23754 updating logging statements * PM-23754 making changes based on PR comments. * updating migration scripts * removing old migration files * update code based testing for whole data object for OrganizationReport and add a stored procedure. * updating services, unit tests, repository tests * fixing unit tests * fixing migration script * fixing migration script again * fixing migration script * another fix * fixing sql file, updating controller to account for different orgIds in the url and body. * updating error message in controllers without a body * making a change to the command * Refactor ReportsController by removing organization reports The IDropOrganizationReportCommand is no longer needed * will code based on PR comments. * fixing unit test * fixing migration script based on last changes. * adding another check in endpoint and adding unit tests * fixing route parameter. * PM-23754 updating data fields to return just the column * PM-23754 fixing repository method signatures * PM-23754 making change to orgId parameter through out code to align with api naming --------- Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com> --- .../OrganizationReportsController.cs | 297 ++ src/Api/Dirt/Controllers/ReportsController.cs | 194 - src/Core/Dirt/Entities/OrganizationReport.cs | 5 +- ...ganizationReportApplicationDataResponse.cs | 6 + .../Data/OrganizationReportDataResponse.cs | 6 + .../OrganizationReportSummaryDataResponse.cs | 6 + .../AddOrganizationReportCommand.cs | 27 +- .../DropOrganizationReportCommand.cs | 45 - ...tOrganizationReportApplicationDataQuery.cs | 62 + .../GetOrganizationReportDataQuery.cs | 62 + .../GetOrganizationReportQuery.cs | 20 +- ...zationReportSummaryDataByDateRangeQuery.cs | 89 + .../GetOrganizationReportSummaryDataQuery.cs | 62 + .../IDropOrganizationReportCommand.cs | 9 - ...tOrganizationReportApplicationDataQuery.cs | 8 + .../IGetOrganizationReportDataQuery.cs | 8 + .../Interfaces/IGetOrganizationReportQuery.cs | 2 +- ...zationReportSummaryDataByDateRangeQuery.cs | 9 + .../IGetOrganizationReportSummaryDataQuery.cs | 8 + ...rganizationReportApplicationDataCommand.cs | 9 + .../IUpdateOrganizationReportCommand.cs | 9 + .../IUpdateOrganizationReportDataCommand.cs | 9 + ...IUpdateOrganizationReportSummaryCommand.cs | 9 + .../ReportingServiceCollectionExtensions.cs | 9 +- .../Requests/AddOrganizationReportRequest.cs | 7 +- ...tionReportSummaryDataByDateRangeRequest.cs | 11 + ...rganizationReportApplicationDataRequest.cs | 11 + .../UpdateOrganizationReportDataRequest.cs | 11 + .../UpdateOrganizationReportRequest.cs | 14 + .../UpdateOrganizationReportSummaryRequest.cs | 11 + ...rganizationReportApplicationDataCommand.cs | 96 + .../UpdateOrganizationReportCommand.cs | 124 + .../UpdateOrganizationReportDataCommand.cs | 96 + .../UpdateOrganizationReportSummaryCommand.cs | 96 + .../IOrganizationReportRepository.cs | 17 +- .../Dirt/OrganizationReportRepository.cs | 148 +- .../OrganizationReportRepository.cs | 166 +- .../OrganizationReport_Create.sql | 53 +- ...anizationReport_GetApplicationDataById.sql | 12 + ...zationReport_GetLatestByOrganizationId.sql | 19 + .../OrganizationReport_GetReportDataById.sql | 12 + ...nizationReport_GetSummariesByDateRange.sql | 17 + .../OrganizationReport_GetSummaryDataById.sql | 13 + ...rganizationReport_ReadByOrganizationId.sql | 9 - .../OrganizationReport_Update.sql | 23 + ...ganizationReport_UpdateApplicationData.sql | 16 + .../OrganizationReport_UpdateReportData.sql | 16 + .../OrganizationReport_UpdateSummaryData.sql | 16 + .../dbo/Dirt/Tables/OrganizationReport.sql | 15 +- .../OrganizationReportsControllerTests.cs | 1165 ++++++ test/Api.Test/Dirt/ReportsControllerTests.cs | 321 -- .../DeleteOrganizationReportCommandTests.cs | 194 - ...nizationReportApplicationDataQueryTests.cs | 116 + .../GetOrganizationReportDataQueryTests.cs | 116 + .../GetOrganizationReportQueryTests.cs | 188 - ...nReportSummaryDataByDateRangeQueryTests.cs | 133 + ...OrganizationReportSummaryDataQueryTests.cs | 116 + ...zationReportApplicationDataCommandTests.cs | 252 ++ .../UpdateOrganizationReportCommandTests.cs | 230 ++ ...pdateOrganizationReportDataCommandTests.cs | 252 ++ ...teOrganizationReportSummaryCommandTests.cs | 252 ++ .../OrganizationReportRepositoryTests.cs | 323 +- .../2025-08-22_00_AlterOrganizationReport.sql | 96 + ..._AddOrganizationReportStoredProcedures.sql | 156 + ...-22_00_AlterOrganizationReport.Designer.cs | 3275 ++++++++++++++++ ...9_2025-08-22_00_AlterOrganizationReport.cs | 49 + ...nizationReportStoredProcedures.Designer.cs | 3275 ++++++++++++++++ ...1_AddOrganizationReportStoredProcedures.cs | 21 + .../DatabaseContextModelSnapshot.cs | 12 +- ...-22_00_AlterOrganizationReport.Designer.cs | 3281 +++++++++++++++++ ...0_2025-08-22_00_AlterOrganizationReport.cs | 47 + ...nizationReportStoredProcedures.Designer.cs | 3281 +++++++++++++++++ ...1_AddOrganizationReportStoredProcedures.cs | 21 + .../DatabaseContextModelSnapshot.cs | 12 +- ...-22_00_AlterOrganizationReport.Designer.cs | 3264 ++++++++++++++++ ...5_2025-08-22_00_AlterOrganizationReport.cs | 47 + ...nizationReportStoredProcedures.Designer.cs | 3264 ++++++++++++++++ ...1_AddOrganizationReportStoredProcedures.cs | 21 + .../DatabaseContextModelSnapshot.cs | 12 +- 79 files changed, 24744 insertions(+), 1047 deletions(-) create mode 100644 src/Api/Dirt/Controllers/OrganizationReportsController.cs create mode 100644 src/Core/Dirt/Models/Data/OrganizationReportApplicationDataResponse.cs create mode 100644 src/Core/Dirt/Models/Data/OrganizationReportDataResponse.cs create mode 100644 src/Core/Dirt/Models/Data/OrganizationReportSummaryDataResponse.cs delete mode 100644 src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportApplicationDataQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportDataQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataQuery.cs delete mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportApplicationDataQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportDataQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataByDateRangeQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataQuery.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportApplicationDataCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportDataCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportSummaryCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/GetOrganizationReportSummaryDataByDateRangeRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportApplicationDataRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportDataRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportSummaryRequest.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportApplicationDataCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportDataCommand.cs create mode 100644 src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportSummaryCommand.cs create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetApplicationDataById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetLatestByOrganizationId.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetReportDataById.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummariesByDateRange.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummaryDataById.sql delete mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Update.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateApplicationData.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateReportData.sql create mode 100644 src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateSummaryData.sql create mode 100644 test/Api.Test/Dirt/OrganizationReportsControllerTests.cs delete mode 100644 test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportApplicationDataQueryTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportDataQueryTests.cs delete mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQueryTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataQueryTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportApplicationDataCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportDataCommandTests.cs create mode 100644 test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportSummaryCommandTests.cs create mode 100644 util/Migrator/DbScripts/2025-08-22_00_AlterOrganizationReport.sql create mode 100644 util/Migrator/DbScripts/2025-08-22_01_AddOrganizationReportStoredProcedures.sql create mode 100644 util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.cs create mode 100644 util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.cs create mode 100644 util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.cs create mode 100644 util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.cs create mode 100644 util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.cs create mode 100644 util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.cs diff --git a/src/Api/Dirt/Controllers/OrganizationReportsController.cs b/src/Api/Dirt/Controllers/OrganizationReportsController.cs new file mode 100644 index 0000000000..bcd64b0bdf --- /dev/null +++ b/src/Api/Dirt/Controllers/OrganizationReportsController.cs @@ -0,0 +1,297 @@ +using Bit.Core.Context; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Exceptions; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Dirt.Controllers; + +[Route("reports/organizations")] +[Authorize("Application")] +public class OrganizationReportsController : Controller +{ + private readonly ICurrentContext _currentContext; + private readonly IGetOrganizationReportQuery _getOrganizationReportQuery; + private readonly IAddOrganizationReportCommand _addOrganizationReportCommand; + private readonly IUpdateOrganizationReportCommand _updateOrganizationReportCommand; + private readonly IUpdateOrganizationReportSummaryCommand _updateOrganizationReportSummaryCommand; + private readonly IGetOrganizationReportSummaryDataQuery _getOrganizationReportSummaryDataQuery; + private readonly IGetOrganizationReportSummaryDataByDateRangeQuery _getOrganizationReportSummaryDataByDateRangeQuery; + private readonly IGetOrganizationReportDataQuery _getOrganizationReportDataQuery; + private readonly IUpdateOrganizationReportDataCommand _updateOrganizationReportDataCommand; + private readonly IGetOrganizationReportApplicationDataQuery _getOrganizationReportApplicationDataQuery; + private readonly IUpdateOrganizationReportApplicationDataCommand _updateOrganizationReportApplicationDataCommand; + + public OrganizationReportsController( + ICurrentContext currentContext, + IGetOrganizationReportQuery getOrganizationReportQuery, + IAddOrganizationReportCommand addOrganizationReportCommand, + IUpdateOrganizationReportCommand updateOrganizationReportCommand, + IUpdateOrganizationReportSummaryCommand updateOrganizationReportSummaryCommand, + IGetOrganizationReportSummaryDataQuery getOrganizationReportSummaryDataQuery, + IGetOrganizationReportSummaryDataByDateRangeQuery getOrganizationReportSummaryDataByDateRangeQuery, + IGetOrganizationReportDataQuery getOrganizationReportDataQuery, + IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand, + IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery, + IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand + ) + { + _currentContext = currentContext; + _getOrganizationReportQuery = getOrganizationReportQuery; + _addOrganizationReportCommand = addOrganizationReportCommand; + _updateOrganizationReportCommand = updateOrganizationReportCommand; + _updateOrganizationReportSummaryCommand = updateOrganizationReportSummaryCommand; + _getOrganizationReportSummaryDataQuery = getOrganizationReportSummaryDataQuery; + _getOrganizationReportSummaryDataByDateRangeQuery = getOrganizationReportSummaryDataByDateRangeQuery; + _getOrganizationReportDataQuery = getOrganizationReportDataQuery; + _updateOrganizationReportDataCommand = updateOrganizationReportDataCommand; + _getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery; + _updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand; + } + + #region Whole OrganizationReport Endpoints + + [HttpGet("{organizationId}/latest")] + public async Task GetLatestOrganizationReportAsync(Guid organizationId) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + var latestReport = await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId); + + return Ok(latestReport); + } + + [HttpGet("{organizationId}/{reportId}")] + public async Task GetOrganizationReportAsync(Guid organizationId, Guid reportId) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId); + + if (report == null) + { + throw new NotFoundException("Report not found for the specified organization."); + } + + if (report.OrganizationId != organizationId) + { + throw new BadRequestException("Invalid report ID"); + } + + return Ok(report); + } + + [HttpPost("{organizationId}")] + public async Task CreateOrganizationReportAsync(Guid organizationId, [FromBody] AddOrganizationReportRequest request) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (request.OrganizationId != organizationId) + { + throw new BadRequestException("Organization ID in the request body must match the route parameter"); + } + + var report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request); + return Ok(report); + } + + [HttpPatch("{organizationId}/{reportId}")] + public async Task UpdateOrganizationReportAsync(Guid organizationId, [FromBody] UpdateOrganizationReportRequest request) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (request.OrganizationId != organizationId) + { + throw new BadRequestException("Organization ID in the request body must match the route parameter"); + } + + var updatedReport = await _updateOrganizationReportCommand.UpdateOrganizationReportAsync(request); + return Ok(updatedReport); + } + + #endregion + + # region SummaryData Field Endpoints + + [HttpGet("{organizationId}/data/summary")] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync( + Guid organizationId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (organizationId.Equals(null)) + { + throw new BadRequestException("Organization ID is required."); + } + + var summaryDataList = await _getOrganizationReportSummaryDataByDateRangeQuery + .GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate); + + return Ok(summaryDataList); + } + + [HttpGet("{organizationId}/data/summary/{reportId}")] + public async Task GetOrganizationReportSummaryAsync(Guid organizationId, Guid reportId) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + var summaryData = + await _getOrganizationReportSummaryDataQuery.GetOrganizationReportSummaryDataAsync(organizationId, reportId); + + if (summaryData == null) + { + throw new NotFoundException("Report not found for the specified organization."); + } + + return Ok(summaryData); + } + + [HttpPatch("{organizationId}/data/summary/{reportId}")] + public async Task UpdateOrganizationReportSummaryAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportSummaryRequest request) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (request.OrganizationId != organizationId) + { + throw new BadRequestException("Organization ID in the request body must match the route parameter"); + } + + if (request.ReportId != reportId) + { + throw new BadRequestException("Report ID in the request body must match the route parameter"); + } + + var updatedReport = await _updateOrganizationReportSummaryCommand.UpdateOrganizationReportSummaryAsync(request); + + return Ok(updatedReport); + } + #endregion + + #region ReportData Field Endpoints + + [HttpGet("{organizationId}/data/report/{reportId}")] + public async Task GetOrganizationReportDataAsync(Guid organizationId, Guid reportId) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + var reportData = await _getOrganizationReportDataQuery.GetOrganizationReportDataAsync(organizationId, reportId); + + if (reportData == null) + { + throw new NotFoundException("Organization report data not found."); + } + + return Ok(reportData); + } + + [HttpPatch("{organizationId}/data/report/{reportId}")] + public async Task UpdateOrganizationReportDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportDataRequest request) + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (request.OrganizationId != organizationId) + { + throw new BadRequestException("Organization ID in the request body must match the route parameter"); + } + + if (request.ReportId != reportId) + { + throw new BadRequestException("Report ID in the request body must match the route parameter"); + } + + var updatedReport = await _updateOrganizationReportDataCommand.UpdateOrganizationReportDataAsync(request); + return Ok(updatedReport); + } + + #endregion + + #region ApplicationData Field Endpoints + + [HttpGet("{organizationId}/data/application/{reportId}")] + public async Task GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId) + { + try + { + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + var applicationData = await _getOrganizationReportApplicationDataQuery.GetOrganizationReportApplicationDataAsync(organizationId, reportId); + + if (applicationData == null) + { + throw new NotFoundException("Organization report application data not found."); + } + + return Ok(applicationData); + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + throw; + } + } + + [HttpPatch("{organizationId}/data/application/{reportId}")] + public async Task UpdateOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportApplicationDataRequest request) + { + try + { + + if (!await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + if (request.OrganizationId != organizationId) + { + throw new BadRequestException("Organization ID in the request body must match the route parameter"); + } + + if (request.Id != reportId) + { + throw new BadRequestException("Report ID in the request body must match the route parameter"); + } + + var updatedReport = await _updateOrganizationReportApplicationDataCommand.UpdateOrganizationReportApplicationDataAsync(request); + + + + return Ok(updatedReport); + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + throw; + } + } + + #endregion +} diff --git a/src/Api/Dirt/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs index d643d68661..3e9f2f0e0d 100644 --- a/src/Api/Dirt/Controllers/ReportsController.cs +++ b/src/Api/Dirt/Controllers/ReportsController.cs @@ -25,7 +25,6 @@ public class ReportsController : Controller private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery; private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand; private readonly IAddOrganizationReportCommand _addOrganizationReportCommand; - private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand; private readonly IGetOrganizationReportQuery _getOrganizationReportQuery; private readonly ILogger _logger; @@ -38,7 +37,6 @@ public class ReportsController : Controller IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand, IGetOrganizationReportQuery getOrganizationReportQuery, IAddOrganizationReportCommand addOrganizationReportCommand, - IDropOrganizationReportCommand dropOrganizationReportCommand, ILogger logger ) { @@ -50,7 +48,6 @@ public class ReportsController : Controller _dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand; _getOrganizationReportQuery = getOrganizationReportQuery; _addOrganizationReportCommand = addOrganizationReportCommand; - _dropOrganizationReportCommand = dropOrganizationReportCommand; _logger = logger; } @@ -209,195 +206,4 @@ public class ReportsController : Controller await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request); } - - /// - /// Adds a new organization report - /// - /// A single instance of AddOrganizationReportRequest - /// A single instance of OrganizationReport - /// If user does not have access to the organization - /// If the organization Id is not valid - [HttpPost("organization-reports")] - public async Task AddOrganizationReport([FromBody] AddOrganizationReportRequest request) - { - if (!await _currentContext.AccessReports(request.OrganizationId)) - { - throw new NotFoundException(); - } - return await _addOrganizationReportCommand.AddOrganizationReportAsync(request); - } - - /// - /// Drops organization reports for an organization - /// - /// A single instance of DropOrganizationReportRequest - /// - /// If user does not have access to the organization - /// If the organization does not have any records - [HttpDelete("organization-reports")] - public async Task DropOrganizationReport([FromBody] DropOrganizationReportRequest request) - { - if (!await _currentContext.AccessReports(request.OrganizationId)) - { - throw new NotFoundException(); - } - await _dropOrganizationReportCommand.DropOrganizationReportAsync(request); - } - - /// - /// Gets organization reports for an organization - /// - /// A valid Organization Id - /// An Enumerable of OrganizationReport - /// If user does not have access to the organization - /// If the organization Id is not valid - [HttpGet("organization-reports/{orgId}")] - public async Task> GetOrganizationReports(Guid orgId) - { - if (!await _currentContext.AccessReports(orgId)) - { - throw new NotFoundException(); - } - return await _getOrganizationReportQuery.GetOrganizationReportAsync(orgId); - } - - /// - /// Gets the latest organization report for an organization - /// - /// A valid Organization Id - /// A single instance of OrganizationReport - /// If user does not have access to the organization - /// If the organization Id is not valid - [HttpGet("organization-reports/latest/{orgId}")] - public async Task GetLatestOrganizationReport(Guid orgId) - { - if (!await _currentContext.AccessReports(orgId)) - { - throw new NotFoundException(); - } - return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId); - } - - /// - /// Gets the Organization Report Summary for an organization. - /// This includes the latest report's encrypted data, encryption key, and date. - /// This is a mock implementation and should be replaced with actual data retrieval logic. - /// - /// - /// Min date (example: 2023-01-01) - /// Max date (example: 2023-12-31) - /// - /// - [HttpGet("organization-report-summary/{orgId}")] - public IEnumerable GetOrganizationReportSummary( - [FromRoute] Guid orgId, - [FromQuery] DateOnly from, - [FromQuery] DateOnly to) - { - if (!ModelState.IsValid) - { - throw new BadRequestException(ModelState); - } - - GuardOrganizationAccess(orgId); - - // FIXME: remove this mock class when actual data retrieval is implemented - return MockOrganizationReportSummary.GetMockData() - .Where(_ => _.OrganizationId == orgId - && _.Date >= from.ToDateTime(TimeOnly.MinValue) - && _.Date <= to.ToDateTime(TimeOnly.MaxValue)); - } - - /// - /// Creates a new Organization Report Summary for an organization. - /// This is a mock implementation and should be replaced with actual creation logic. - /// - /// - /// Returns 204 Created with the created OrganizationReportSummaryModel - /// - [HttpPost("organization-report-summary")] - public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) - { - if (!ModelState.IsValid) - { - throw new BadRequestException(ModelState); - } - - GuardOrganizationAccess(model.OrganizationId); - - // TODO: Implement actual creation logic - - // Returns 204 No Content as a placeholder - return NoContent(); - } - - [HttpPut("organization-report-summary")] - public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model) - { - if (!ModelState.IsValid) - { - throw new BadRequestException(ModelState); - } - - GuardOrganizationAccess(model.OrganizationId); - - // TODO: Implement actual update logic - - // Returns 204 No Content as a placeholder - return NoContent(); - } - - private void GuardOrganizationAccess(Guid organizationId) - { - if (!_currentContext.AccessReports(organizationId).Result) - { - throw new NotFoundException(); - } - } - - // FIXME: remove this mock class when actual data retrieval is implemented - private class MockOrganizationReportSummary - { - public static List GetMockData() - { - return new List - { - new OrganizationReportSummaryModel - { - OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), - EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=", - EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=", - Date = DateTime.UtcNow - }, - new OrganizationReportSummaryModel - { - OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), - EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=", - EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=", - Date = DateTime.UtcNow.AddMonths(-1) - }, - new OrganizationReportSummaryModel - { - OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), - EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=", - EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=", - Date = DateTime.UtcNow.AddMonths(-1) - }, - new OrganizationReportSummaryModel - { - OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), - EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=", - EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=", - Date = DateTime.UtcNow.AddMonths(-1) - }, - new OrganizationReportSummaryModel - { - OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"), - EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=", - EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=", - Date = DateTime.UtcNow.AddMonths(-1) - }, - }; - } - } } diff --git a/src/Core/Dirt/Entities/OrganizationReport.cs b/src/Core/Dirt/Entities/OrganizationReport.cs index 92975ca441..a776648b35 100644 --- a/src/Core/Dirt/Entities/OrganizationReport.cs +++ b/src/Core/Dirt/Entities/OrganizationReport.cs @@ -9,12 +9,15 @@ public class OrganizationReport : ITableObject { public Guid Id { get; set; } public Guid OrganizationId { get; set; } - public DateTime Date { get; set; } public string ReportData { get; set; } = string.Empty; public DateTime CreationDate { get; set; } = DateTime.UtcNow; public string ContentEncryptionKey { get; set; } = string.Empty; + public string? SummaryData { get; set; } = null; + public string? ApplicationData { get; set; } = null; + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + public void SetNewId() { Id = CoreHelpers.GenerateComb(); diff --git a/src/Core/Dirt/Models/Data/OrganizationReportApplicationDataResponse.cs b/src/Core/Dirt/Models/Data/OrganizationReportApplicationDataResponse.cs new file mode 100644 index 0000000000..292d8e6f38 --- /dev/null +++ b/src/Core/Dirt/Models/Data/OrganizationReportApplicationDataResponse.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Dirt.Models.Data; + +public class OrganizationReportApplicationDataResponse +{ + public string? ApplicationData { get; set; } +} diff --git a/src/Core/Dirt/Models/Data/OrganizationReportDataResponse.cs b/src/Core/Dirt/Models/Data/OrganizationReportDataResponse.cs new file mode 100644 index 0000000000..c284d99ff2 --- /dev/null +++ b/src/Core/Dirt/Models/Data/OrganizationReportDataResponse.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Dirt.Models.Data; + +public class OrganizationReportDataResponse +{ + public string? ReportData { get; set; } +} diff --git a/src/Core/Dirt/Models/Data/OrganizationReportSummaryDataResponse.cs b/src/Core/Dirt/Models/Data/OrganizationReportSummaryDataResponse.cs new file mode 100644 index 0000000000..0533c2862f --- /dev/null +++ b/src/Core/Dirt/Models/Data/OrganizationReportSummaryDataResponse.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Dirt.Models.Data; + +public class OrganizationReportSummaryDataResponse +{ + public string? SummaryData { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs index 66d25cdf56..f0477806d8 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/AddOrganizationReportCommand.cs @@ -26,12 +26,12 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand public async Task AddOrganizationReportAsync(AddOrganizationReportRequest request) { - _logger.LogInformation("Adding organization report for organization {organizationId}", request.OrganizationId); + _logger.LogInformation(Constants.BypassFiltersEventId, "Adding organization report for organization {organizationId}", request.OrganizationId); var (isValid, errorMessage) = await ValidateRequestAsync(request); if (!isValid) { - _logger.LogInformation("Failed to add organization {organizationId} report: {errorMessage}", request.OrganizationId, errorMessage); + _logger.LogInformation(Constants.BypassFiltersEventId, "Failed to add organization {organizationId} report: {errorMessage}", request.OrganizationId, errorMessage); throw new BadRequestException(errorMessage); } @@ -39,15 +39,18 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand { OrganizationId = request.OrganizationId, ReportData = request.ReportData, - Date = request.Date == default ? DateTime.UtcNow : request.Date, CreationDate = DateTime.UtcNow, + ContentEncryptionKey = request.ContentEncryptionKey, + SummaryData = request.SummaryData, + ApplicationData = request.ApplicationData, + RevisionDate = DateTime.UtcNow }; organizationReport.SetNewId(); var data = await _organizationReportRepo.CreateAsync(organizationReport); - _logger.LogInformation("Successfully added organization report for organization {organizationId}, {organizationReportId}", + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully added organization report for organization {organizationId}, {organizationReportId}", request.OrganizationId, data.Id); return data; @@ -63,12 +66,26 @@ public class AddOrganizationReportCommand : IAddOrganizationReportCommand return (false, "Invalid Organization"); } - // ensure that we have report data + if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey)) + { + return (false, "Content Encryption Key is required"); + } + if (string.IsNullOrWhiteSpace(request.ReportData)) { return (false, "Report Data is required"); } + if (string.IsNullOrWhiteSpace(request.SummaryData)) + { + return (false, "Summary Data is required"); + } + + if (string.IsNullOrWhiteSpace(request.ApplicationData)) + { + return (false, "Application Data is required"); + } + return (true, string.Empty); } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs deleted file mode 100644 index 8fe206c1f1..0000000000 --- a/src/Core/Dirt/Reports/ReportFeatures/DropOrganizationReportCommand.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; -using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Repositories; -using Bit.Core.Exceptions; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Dirt.Reports.ReportFeatures; - -public class DropOrganizationReportCommand : IDropOrganizationReportCommand -{ - private IOrganizationReportRepository _organizationReportRepo; - private ILogger _logger; - - public DropOrganizationReportCommand( - IOrganizationReportRepository organizationReportRepository, - ILogger logger) - { - _organizationReportRepo = organizationReportRepository; - _logger = logger; - } - - public async Task DropOrganizationReportAsync(DropOrganizationReportRequest request) - { - _logger.LogInformation("Dropping organization report for organization {organizationId}", - request.OrganizationId); - - var data = await _organizationReportRepo.GetByOrganizationIdAsync(request.OrganizationId); - if (data == null || data.Count() == 0) - { - _logger.LogInformation("No organization reports found for organization {organizationId}", request.OrganizationId); - throw new BadRequestException("No data found."); - } - - data - .Where(_ => request.OrganizationReportIds.Contains(_.Id)) - .ToList() - .ForEach(async reportId => - { - _logger.LogInformation("Dropping organization report {organizationReportId} for organization {organizationId}", - reportId, request.OrganizationId); - - await _organizationReportRepo.DeleteAsync(reportId); - }); - } -} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportApplicationDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportApplicationDataQuery.cs new file mode 100644 index 0000000000..983fa71fd7 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportApplicationDataQuery.cs @@ -0,0 +1,62 @@ +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class GetOrganizationReportApplicationDataQuery : IGetOrganizationReportApplicationDataQuery +{ + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public GetOrganizationReportApplicationDataQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report application data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + if (organizationId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportApplicationDataAsync called with empty OrganizationId"); + throw new BadRequestException("OrganizationId is required."); + } + + if (reportId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportApplicationDataAsync called with empty ReportId"); + throw new BadRequestException("ReportId is required."); + } + + var applicationDataResponse = await _organizationReportRepo.GetApplicationDataAsync(reportId); + + if (applicationDataResponse == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "No application data found for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw new NotFoundException("Organization report application data not found."); + } + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report application data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + return applicationDataResponse; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error fetching organization report application data for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw; + } + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportDataQuery.cs new file mode 100644 index 0000000000..d53fa56111 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportDataQuery.cs @@ -0,0 +1,62 @@ +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class GetOrganizationReportDataQuery : IGetOrganizationReportDataQuery +{ + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public GetOrganizationReportDataQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task GetOrganizationReportDataAsync(Guid organizationId, Guid reportId) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + if (organizationId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportDataAsync called with empty OrganizationId"); + throw new BadRequestException("OrganizationId is required."); + } + + if (reportId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportDataAsync called with empty ReportId"); + throw new BadRequestException("ReportId is required."); + } + + var reportDataResponse = await _organizationReportRepo.GetReportDataAsync(reportId); + + if (reportDataResponse == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "No report data found for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw new NotFoundException("Organization report data not found."); + } + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + return reportDataResponse; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error fetching organization report data for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw; + } + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs index e536fdfddc..b0bf9e450a 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportQuery.cs @@ -19,15 +19,23 @@ public class GetOrganizationReportQuery : IGetOrganizationReportQuery _logger = logger; } - public async Task> GetOrganizationReportAsync(Guid organizationId) + public async Task GetOrganizationReportAsync(Guid reportId) { - if (organizationId == Guid.Empty) + if (reportId == Guid.Empty) { - throw new BadRequestException("OrganizationId is required."); + throw new BadRequestException("Id of report is required."); } - _logger.LogInformation("Fetching organization reports for organization {organizationId}", organizationId); - return await _organizationReportRepo.GetByOrganizationIdAsync(organizationId); + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization reports for organization by Id: {reportId}", reportId); + + var results = await _organizationReportRepo.GetByIdAsync(reportId); + + if (results == null) + { + throw new NotFoundException($"No report found for Id: {reportId}"); + } + + return results; } public async Task GetLatestOrganizationReportAsync(Guid organizationId) @@ -37,7 +45,7 @@ public class GetOrganizationReportQuery : IGetOrganizationReportQuery throw new BadRequestException("OrganizationId is required."); } - _logger.LogInformation("Fetching latest organization report for organization {organizationId}", organizationId); + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching latest organization report for organization {organizationId}", organizationId); return await _organizationReportRepo.GetLatestByOrganizationIdAsync(organizationId); } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQuery.cs new file mode 100644 index 0000000000..7be59b822e --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQuery.cs @@ -0,0 +1,89 @@ +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class GetOrganizationReportSummaryDataByDateRangeQuery : IGetOrganizationReportSummaryDataByDateRangeQuery +{ + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public GetOrganizationReportSummaryDataByDateRangeQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task> GetOrganizationReportSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report summary data by date range for organization {organizationId}, from {startDate} to {endDate}", + organizationId, startDate, endDate); + + var (isValid, errorMessage) = ValidateRequest(organizationId, startDate, endDate); + if (!isValid) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataByDateRangeAsync validation failed: {errorMessage}", errorMessage); + throw new BadRequestException(errorMessage); + } + + IEnumerable summaryDataList = (await _organizationReportRepo + .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate)) ?? + Enumerable.Empty(); + + var resultList = summaryDataList.ToList(); + + if (!resultList.Any()) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "No summary data found for organization {organizationId} in date range {startDate} to {endDate}", + organizationId, startDate, endDate); + return Enumerable.Empty(); + } + else + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved {count} organization report summary data records for organization {organizationId} in date range {startDate} to {endDate}", + resultList.Count, organizationId, startDate, endDate); + + } + + return resultList; + } + catch (Exception ex) when (!(ex is BadRequestException)) + { + _logger.LogError(ex, "Error fetching organization report summary data by date range for organization {organizationId}, from {startDate} to {endDate}", + organizationId, startDate, endDate); + throw; + } + } + + private static (bool IsValid, string errorMessage) ValidateRequest(Guid organizationId, DateTime startDate, DateTime endDate) + { + if (organizationId == Guid.Empty) + { + return (false, "OrganizationId is required"); + } + + if (startDate == default) + { + return (false, "StartDate is required"); + } + + if (endDate == default) + { + return (false, "EndDate is required"); + } + + if (startDate > endDate) + { + return (false, "StartDate must be earlier than or equal to EndDate"); + } + + return (true, string.Empty); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataQuery.cs new file mode 100644 index 0000000000..83ee24a476 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/GetOrganizationReportSummaryDataQuery.cs @@ -0,0 +1,62 @@ +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class GetOrganizationReportSummaryDataQuery : IGetOrganizationReportSummaryDataQuery +{ + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public GetOrganizationReportSummaryDataQuery( + IOrganizationReportRepository organizationReportRepo, + ILogger logger) + { + _organizationReportRepo = organizationReportRepo; + _logger = logger; + } + + public async Task GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Fetching organization report summary data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + if (organizationId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataAsync called with empty OrganizationId"); + throw new BadRequestException("OrganizationId is required."); + } + + if (reportId == Guid.Empty) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "GetOrganizationReportSummaryDataAsync called with empty ReportId"); + throw new BadRequestException("ReportId is required."); + } + + var summaryDataResponse = await _organizationReportRepo.GetSummaryDataAsync(reportId); + + if (summaryDataResponse == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "No summary data found for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw new NotFoundException("Organization report summary data not found."); + } + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully retrieved organization report summary data for organization {organizationId} and report {reportId}", + organizationId, reportId); + + return summaryDataResponse; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error fetching organization report summary data for organization {organizationId} and report {reportId}", + organizationId, reportId); + throw; + } + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs deleted file mode 100644 index 1ed9059f56..0000000000 --- a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropOrganizationReportCommand.cs +++ /dev/null @@ -1,9 +0,0 @@ - -using Bit.Core.Dirt.Reports.ReportFeatures.Requests; - -namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; - -public interface IDropOrganizationReportCommand -{ - Task DropOrganizationReportAsync(DropOrganizationReportRequest request); -} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportApplicationDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportApplicationDataQuery.cs new file mode 100644 index 0000000000..f7eceea583 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportApplicationDataQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.Dirt.Models.Data; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportApplicationDataQuery +{ + Task GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportDataQuery.cs new file mode 100644 index 0000000000..3817fa03d2 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportDataQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.Dirt.Models.Data; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportDataQuery +{ + Task GetOrganizationReportDataAsync(Guid organizationId, Guid reportId); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs index f596e8f517..b72fdd25b5 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportQuery.cs @@ -4,6 +4,6 @@ namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; public interface IGetOrganizationReportQuery { - Task> GetOrganizationReportAsync(Guid organizationId); + Task GetOrganizationReportAsync(Guid organizationId); Task GetLatestOrganizationReportAsync(Guid organizationId); } diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataByDateRangeQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataByDateRangeQuery.cs new file mode 100644 index 0000000000..2659a3d78b --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataByDateRangeQuery.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Models.Data; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportSummaryDataByDateRangeQuery +{ + Task> GetOrganizationReportSummaryDataByDateRangeAsync( + Guid organizationId, DateTime startDate, DateTime endDate); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataQuery.cs new file mode 100644 index 0000000000..8b208c8a8a --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetOrganizationReportSummaryDataQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.Dirt.Models.Data; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IGetOrganizationReportSummaryDataQuery +{ + Task GetOrganizationReportSummaryDataAsync(Guid organizationId, Guid reportId); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportApplicationDataCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportApplicationDataCommand.cs new file mode 100644 index 0000000000..352de679be --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportApplicationDataCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IUpdateOrganizationReportApplicationDataCommand +{ + Task UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportCommand.cs new file mode 100644 index 0000000000..fc947b9f9d --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IUpdateOrganizationReportCommand +{ + Task UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportDataCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportDataCommand.cs new file mode 100644 index 0000000000..cb212714f2 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportDataCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IUpdateOrganizationReportDataCommand +{ + Task UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportSummaryCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportSummaryCommand.cs new file mode 100644 index 0000000000..bdc2081a1f --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IUpdateOrganizationReportSummaryCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; + +public interface IUpdateOrganizationReportSummaryCommand +{ + Task UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request); +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs index a20c7a3e8f..f89ff97762 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs @@ -14,7 +14,14 @@ public static class ReportingServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs index f5a3d581f2..2a8c0203f9 100644 --- a/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddOrganizationReportRequest.cs @@ -7,5 +7,10 @@ public class AddOrganizationReportRequest { public Guid OrganizationId { get; set; } public string ReportData { get; set; } - public DateTime Date { get; set; } + + public string ContentEncryptionKey { get; set; } + + public string SummaryData { get; set; } + + public string ApplicationData { get; set; } } diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/GetOrganizationReportSummaryDataByDateRangeRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/GetOrganizationReportSummaryDataByDateRangeRequest.cs new file mode 100644 index 0000000000..8949cfdff3 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/GetOrganizationReportSummaryDataByDateRangeRequest.cs @@ -0,0 +1,11 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class GetOrganizationReportSummaryDataByDateRangeRequest +{ + public Guid OrganizationId { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportApplicationDataRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportApplicationDataRequest.cs new file mode 100644 index 0000000000..ab4fcc5921 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportApplicationDataRequest.cs @@ -0,0 +1,11 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class UpdateOrganizationReportApplicationDataRequest +{ + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public string ApplicationData { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportDataRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportDataRequest.cs new file mode 100644 index 0000000000..673a3f2ab8 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportDataRequest.cs @@ -0,0 +1,11 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class UpdateOrganizationReportDataRequest +{ + public Guid OrganizationId { get; set; } + public Guid ReportId { get; set; } + public string ReportData { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportRequest.cs new file mode 100644 index 0000000000..501f5a1a1a --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportRequest.cs @@ -0,0 +1,14 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class UpdateOrganizationReportRequest +{ + public Guid ReportId { get; set; } + public Guid OrganizationId { get; set; } + public string ReportData { get; set; } + public string ContentEncryptionKey { get; set; } + public string SummaryData { get; set; } = null; + public string ApplicationData { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportSummaryRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportSummaryRequest.cs new file mode 100644 index 0000000000..b0e555fcef --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/Requests/UpdateOrganizationReportSummaryRequest.cs @@ -0,0 +1,11 @@ +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable + +namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests; + +public class UpdateOrganizationReportSummaryRequest +{ + public Guid OrganizationId { get; set; } + public Guid ReportId { get; set; } + public string SummaryData { get; set; } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportApplicationDataCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportApplicationDataCommand.cs new file mode 100644 index 0000000000..67ec49d004 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportApplicationDataCommand.cs @@ -0,0 +1,96 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class UpdateOrganizationReportApplicationDataCommand : IUpdateOrganizationReportApplicationDataCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public UpdateOrganizationReportApplicationDataCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task UpdateOrganizationReportApplicationDataAsync(UpdateOrganizationReportApplicationDataRequest request) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report application data {reportId} for organization {organizationId}", + request.Id, request.OrganizationId); + + var (isValid, errorMessage) = await ValidateRequestAsync(request); + if (!isValid) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report application data {reportId} for organization {organizationId}: {errorMessage}", + request.Id, request.OrganizationId, errorMessage); + throw new BadRequestException(errorMessage); + } + + var existingReport = await _organizationReportRepo.GetByIdAsync(request.Id); + if (existingReport == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.Id); + throw new NotFoundException("Organization report not found"); + } + + if (existingReport.OrganizationId != request.OrganizationId) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}", + request.Id, request.OrganizationId); + throw new BadRequestException("Organization report does not belong to the specified organization"); + } + + var updatedReport = await _organizationReportRepo.UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData); + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report application data {reportId} for organization {organizationId}", + request.Id, request.OrganizationId); + + return updatedReport; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error updating organization report application data {reportId} for organization {organizationId}", + request.Id, request.OrganizationId); + throw; + } + } + + private async Task<(bool isValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportApplicationDataRequest request) + { + if (request.OrganizationId == Guid.Empty) + { + return (false, "OrganizationId is required"); + } + + if (request.Id == Guid.Empty) + { + return (false, "Id is required"); + } + + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return (false, "Invalid Organization"); + } + + if (string.IsNullOrWhiteSpace(request.ApplicationData)) + { + return (false, "Application Data is required"); + } + + return (true, string.Empty); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportCommand.cs new file mode 100644 index 0000000000..7fb77030a8 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportCommand.cs @@ -0,0 +1,124 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class UpdateOrganizationReportCommand : IUpdateOrganizationReportCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public UpdateOrganizationReportCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task UpdateOrganizationReportAsync(UpdateOrganizationReportRequest request) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + var (isValid, errorMessage) = await ValidateRequestAsync(request); + if (!isValid) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report {reportId} for organization {organizationId}: {errorMessage}", + request.ReportId, request.OrganizationId, errorMessage); + throw new BadRequestException(errorMessage); + } + + var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId); + if (existingReport == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId); + throw new NotFoundException("Organization report not found"); + } + + if (existingReport.OrganizationId != request.OrganizationId) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}", + request.ReportId, request.OrganizationId); + throw new BadRequestException("Organization report does not belong to the specified organization"); + } + + existingReport.ContentEncryptionKey = request.ContentEncryptionKey; + existingReport.SummaryData = request.SummaryData; + existingReport.ReportData = request.ReportData; + existingReport.ApplicationData = request.ApplicationData; + existingReport.RevisionDate = DateTime.UtcNow; + + await _organizationReportRepo.UpsertAsync(existingReport); + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + var response = await _organizationReportRepo.GetByIdAsync(request.ReportId); + + if (response == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found after update", request.ReportId); + throw new NotFoundException("Organization report not found after update"); + } + return response; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error updating organization report {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + throw; + } + } + + private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportRequest request) + { + if (request.OrganizationId == Guid.Empty) + { + return (false, "OrganizationId is required"); + } + + if (request.ReportId == Guid.Empty) + { + return (false, "ReportId is required"); + } + + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return (false, "Invalid Organization"); + } + + if (string.IsNullOrWhiteSpace(request.ContentEncryptionKey)) + { + return (false, "ContentEncryptionKey is required"); + } + + if (string.IsNullOrWhiteSpace(request.ReportData)) + { + return (false, "Report Data is required"); + } + + if (string.IsNullOrWhiteSpace(request.SummaryData)) + { + return (false, "Summary Data is required"); + } + + if (string.IsNullOrWhiteSpace(request.ApplicationData)) + { + return (false, "Application Data is required"); + } + + return (true, string.Empty); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportDataCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportDataCommand.cs new file mode 100644 index 0000000000..f81d24c3d7 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportDataCommand.cs @@ -0,0 +1,96 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class UpdateOrganizationReportDataCommand : IUpdateOrganizationReportDataCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public UpdateOrganizationReportDataCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task UpdateOrganizationReportDataAsync(UpdateOrganizationReportDataRequest request) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report data {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + var (isValid, errorMessage) = await ValidateRequestAsync(request); + if (!isValid) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report data {reportId} for organization {organizationId}: {errorMessage}", + request.ReportId, request.OrganizationId, errorMessage); + throw new BadRequestException(errorMessage); + } + + var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId); + if (existingReport == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId); + throw new NotFoundException("Organization report not found"); + } + + if (existingReport.OrganizationId != request.OrganizationId) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}", + request.ReportId, request.OrganizationId); + throw new BadRequestException("Organization report does not belong to the specified organization"); + } + + var updatedReport = await _organizationReportRepo.UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData); + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report data {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + return updatedReport; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error updating organization report data {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + throw; + } + } + + private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportDataRequest request) + { + if (request.OrganizationId == Guid.Empty) + { + return (false, "OrganizationId is required"); + } + + if (request.ReportId == Guid.Empty) + { + return (false, "ReportId is required"); + } + + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return (false, "Invalid Organization"); + } + + if (string.IsNullOrWhiteSpace(request.ReportData)) + { + return (false, "Report Data is required"); + } + + return (true, string.Empty); + } +} diff --git a/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportSummaryCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportSummaryCommand.cs new file mode 100644 index 0000000000..6859814d65 --- /dev/null +++ b/src/Core/Dirt/Reports/ReportFeatures/UpdateOrganizationReportSummaryCommand.cs @@ -0,0 +1,96 @@ +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Dirt.Reports.ReportFeatures; + +public class UpdateOrganizationReportSummaryCommand : IUpdateOrganizationReportSummaryCommand +{ + private readonly IOrganizationRepository _organizationRepo; + private readonly IOrganizationReportRepository _organizationReportRepo; + private readonly ILogger _logger; + + public UpdateOrganizationReportSummaryCommand( + IOrganizationRepository organizationRepository, + IOrganizationReportRepository organizationReportRepository, + ILogger logger) + { + _organizationRepo = organizationRepository; + _organizationReportRepo = organizationReportRepository; + _logger = logger; + } + + public async Task UpdateOrganizationReportSummaryAsync(UpdateOrganizationReportSummaryRequest request) + { + try + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating organization report summary {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + var (isValid, errorMessage) = await ValidateRequestAsync(request); + if (!isValid) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Failed to update organization report summary {reportId} for organization {organizationId}: {errorMessage}", + request.ReportId, request.OrganizationId, errorMessage); + throw new BadRequestException(errorMessage); + } + + var existingReport = await _organizationReportRepo.GetByIdAsync(request.ReportId); + if (existingReport == null) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} not found", request.ReportId); + throw new NotFoundException("Organization report not found"); + } + + if (existingReport.OrganizationId != request.OrganizationId) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Organization report {reportId} does not belong to organization {organizationId}", + request.ReportId, request.OrganizationId); + throw new BadRequestException("Organization report does not belong to the specified organization"); + } + + var updatedReport = await _organizationReportRepo.UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData); + + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated organization report summary {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + + return updatedReport; + } + catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException)) + { + _logger.LogError(ex, "Error updating organization report summary {reportId} for organization {organizationId}", + request.ReportId, request.OrganizationId); + throw; + } + } + + private async Task<(bool IsValid, string errorMessage)> ValidateRequestAsync(UpdateOrganizationReportSummaryRequest request) + { + if (request.OrganizationId == Guid.Empty) + { + return (false, "OrganizationId is required"); + } + + if (request.ReportId == Guid.Empty) + { + return (false, "ReportId is required"); + } + + var organization = await _organizationRepo.GetByIdAsync(request.OrganizationId); + if (organization == null) + { + return (false, "Invalid Organization"); + } + + if (string.IsNullOrWhiteSpace(request.SummaryData)) + { + return (false, "Summary Data is required"); + } + + return (true, string.Empty); + } +} diff --git a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs index e7979ca4b7..9687173716 100644 --- a/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs +++ b/src/Core/Dirt/Repositories/IOrganizationReportRepository.cs @@ -1,12 +1,25 @@ using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data; using Bit.Core.Repositories; namespace Bit.Core.Dirt.Repositories; public interface IOrganizationReportRepository : IRepository { - Task> GetByOrganizationIdAsync(Guid organizationId); - + // Whole OrganizationReport methods Task GetLatestByOrganizationIdAsync(Guid organizationId); + + // SummaryData methods + Task> GetSummaryDataByDateRangeAsync(Guid organizationId, DateTime startDate, DateTime endDate); + Task GetSummaryDataAsync(Guid reportId); + Task UpdateSummaryDataAsync(Guid orgId, Guid reportId, string summaryData); + + // ReportData methods + Task GetReportDataAsync(Guid reportId); + Task UpdateReportDataAsync(Guid orgId, Guid reportId, string reportData); + + // ApplicationData methods + Task GetApplicationDataAsync(Guid reportId); + Task UpdateApplicationDataAsync(Guid orgId, Guid reportId, string applicationData); } diff --git a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs index 2ce17a9983..3d001cce92 100644 --- a/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs +++ b/src/Infrastructure.Dapper/Dirt/OrganizationReportRepository.cs @@ -3,6 +3,7 @@ using System.Data; using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data; using Bit.Core.Dirt.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; @@ -23,26 +24,153 @@ public class OrganizationReportRepository : Repository { } - public async Task> GetByOrganizationIdAsync(Guid organizationId) + public async Task GetLatestByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ReadOnlyConnectionString)) { - var results = await connection.QueryAsync( - $"[{Schema}].[OrganizationReport_ReadByOrganizationId]", + var result = await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_GetLatestByOrganizationId]", new { OrganizationId = organizationId }, commandType: CommandType.StoredProcedure); - return results.ToList(); + return result; } } - public async Task GetLatestByOrganizationIdAsync(Guid organizationId) + public async Task UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData) { - return await GetByOrganizationIdAsync(organizationId) - .ContinueWith(task => + using (var connection = new SqlConnection(ConnectionString)) { - var reports = task.Result; - return reports.OrderByDescending(r => r.CreationDate).FirstOrDefault(); - }); + var parameters = new + { + Id = reportId, + OrganizationId = organizationId, + SummaryData = summaryData, + RevisionDate = DateTime.UtcNow + }; + + await connection.ExecuteAsync( + $"[{Schema}].[OrganizationReport_UpdateSummaryData]", + parameters, + commandType: CommandType.StoredProcedure); + + // Return the updated report + return await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_ReadById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task GetSummaryDataAsync(Guid reportId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_GetSummaryDataById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } + + public async Task> GetSummaryDataByDateRangeAsync( + Guid organizationId, + DateTime startDate, DateTime + endDate) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var parameters = new + { + OrganizationId = organizationId, + StartDate = startDate, + EndDate = endDate + }; + + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationReport_GetSummariesByDateRange]", + parameters, + commandType: CommandType.StoredProcedure); + + return results; + } + } + + public async Task GetReportDataAsync(Guid reportId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_GetReportDataById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } + + public async Task UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var parameters = new + { + OrganizationId = organizationId, + Id = reportId, + ReportData = reportData, + RevisionDate = DateTime.UtcNow + }; + + await connection.ExecuteAsync( + $"[{Schema}].[OrganizationReport_UpdateReportData]", + parameters, + commandType: CommandType.StoredProcedure); + + // Return the updated report + return await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_ReadById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task GetApplicationDataAsync(Guid reportId) + { + using (var connection = new SqlConnection(ReadOnlyConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_GetApplicationDataById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } + + public async Task UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var parameters = new + { + OrganizationId = organizationId, + Id = reportId, + ApplicationData = applicationData, + RevisionDate = DateTime.UtcNow + }; + + await connection.ExecuteAsync( + $"[{Schema}].[OrganizationReport_UpdateApplicationData]", + parameters, + commandType: CommandType.StoredProcedure); + + // Return the updated report + return await connection.QuerySingleOrDefaultAsync( + $"[{Schema}].[OrganizationReport_ReadById]", + new { Id = reportId }, + commandType: CommandType.StoredProcedure); + } } } diff --git a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs index c8e5432e03..525c5a479d 100644 --- a/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs +++ b/src/Infrastructure.EntityFramework/Dirt/Repositories/OrganizationReportRepository.cs @@ -3,6 +3,7 @@ using AutoMapper; using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data; using Bit.Core.Dirt.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using LinqToDB; @@ -19,18 +20,6 @@ public class OrganizationReportRepository : IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationReports) { } - public async Task> GetByOrganizationIdAsync(Guid organizationId) - { - using (var scope = ServiceScopeFactory.CreateScope()) - { - var dbContext = GetDatabaseContext(scope); - var results = await dbContext.OrganizationReports - .Where(p => p.OrganizationId == organizationId) - .ToListAsync(); - return Mapper.Map>(results); - } - } - public async Task GetLatestByOrganizationIdAsync(Guid organizationId) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -38,14 +27,161 @@ public class OrganizationReportRepository : var dbContext = GetDatabaseContext(scope); var result = await dbContext.OrganizationReports .Where(p => p.OrganizationId == organizationId) - .OrderByDescending(p => p.Date) + .OrderByDescending(p => p.RevisionDate) .Take(1) .FirstOrDefaultAsync(); - if (result == null) - return default; + if (result == null) return default; return Mapper.Map(result); } } + + public async Task UpdateSummaryDataAsync(Guid organizationId, Guid reportId, string summaryData) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + // Update only SummaryData and RevisionDate + await dbContext.OrganizationReports + .Where(p => p.Id == reportId && p.OrganizationId == organizationId) + .UpdateAsync(p => new Models.OrganizationReport + { + SummaryData = summaryData, + RevisionDate = DateTime.UtcNow + }); + + // Return the updated report + var updatedReport = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .FirstOrDefaultAsync(); + + return Mapper.Map(updatedReport); + } + } + + public async Task GetSummaryDataAsync(Guid reportId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var result = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .Select(p => new OrganizationReportSummaryDataResponse + { + SummaryData = p.SummaryData + }) + .FirstOrDefaultAsync(); + + return result; + } + } + + public async Task> GetSummaryDataByDateRangeAsync( + Guid organizationId, + DateTime startDate, + DateTime endDate) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var results = await dbContext.OrganizationReports + .Where(p => p.OrganizationId == organizationId && + p.CreationDate >= startDate && p.CreationDate <= endDate) + .Select(p => new OrganizationReportSummaryDataResponse + { + SummaryData = p.SummaryData + }) + .ToListAsync(); + + return results; + } + } + + public async Task GetReportDataAsync(Guid reportId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var result = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .Select(p => new OrganizationReportDataResponse + { + ReportData = p.ReportData + }) + .FirstOrDefaultAsync(); + + return result; + } + } + + public async Task UpdateReportDataAsync(Guid organizationId, Guid reportId, string reportData) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + // Update only ReportData and RevisionDate + await dbContext.OrganizationReports + .Where(p => p.Id == reportId && p.OrganizationId == organizationId) + .UpdateAsync(p => new Models.OrganizationReport + { + ReportData = reportData, + RevisionDate = DateTime.UtcNow + }); + + // Return the updated report + var updatedReport = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .FirstOrDefaultAsync(); + + return Mapper.Map(updatedReport); + } + } + + public async Task GetApplicationDataAsync(Guid reportId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + var result = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .Select(p => new OrganizationReportApplicationDataResponse + { + ApplicationData = p.ApplicationData + }) + .FirstOrDefaultAsync(); + + return result; + } + } + + public async Task UpdateApplicationDataAsync(Guid organizationId, Guid reportId, string applicationData) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + + // Update only ApplicationData and RevisionDate + await dbContext.OrganizationReports + .Where(p => p.Id == reportId && p.OrganizationId == organizationId) + .UpdateAsync(p => new Models.OrganizationReport + { + ApplicationData = applicationData, + RevisionDate = DateTime.UtcNow + }); + + // Return the updated report + var updatedReport = await dbContext.OrganizationReports + .Where(p => p.Id == reportId) + .FirstOrDefaultAsync(); + + return Mapper.Map(updatedReport); + } + } } diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql index 087d4b1e09..d6cd206558 100644 --- a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Create.sql @@ -1,26 +1,35 @@ CREATE PROCEDURE [dbo].[OrganizationReport_Create] - @Id UNIQUEIDENTIFIER OUTPUT, - @OrganizationId UNIQUEIDENTIFIER, - @Date DATETIME2(7), - @ReportData NVARCHAR(MAX), - @CreationDate DATETIME2(7), - @ContentEncryptionKey VARCHAR(MAX) + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX), + @SummaryData NVARCHAR(MAX), + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) AS - SET NOCOUNT ON; +BEGIN + SET NOCOUNT ON; - INSERT INTO [dbo].[OrganizationReport]( - [Id], - [OrganizationId], - [Date], - [ReportData], - [CreationDate], - [ContentEncryptionKey] - ) - VALUES ( - @Id, - @OrganizationId, - @Date, - @ReportData, - @CreationDate, - @ContentEncryptionKey + +INSERT INTO [dbo].[OrganizationReport]( + [Id], + [OrganizationId], + [ReportData], + [CreationDate], + [ContentEncryptionKey], + [SummaryData], + [ApplicationData], + [RevisionDate] +) +VALUES ( + @Id, + @OrganizationId, + @ReportData, + @CreationDate, + @ContentEncryptionKey, + @SummaryData, + @ApplicationData, + @RevisionDate ); +END diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetApplicationDataById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetApplicationDataById.sql new file mode 100644 index 0000000000..83c97b76ee --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetApplicationDataById.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_GetApplicationDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [ApplicationData] + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id +END + diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetLatestByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetLatestByOrganizationId.sql new file mode 100644 index 0000000000..1312369fa8 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetLatestByOrganizationId.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_GetLatestByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + [Id], + [OrganizationId], + [ReportData], + [CreationDate], + [ContentEncryptionKey], + [SummaryData], + [ApplicationData], + [RevisionDate] + FROM [dbo].[OrganizationReportView] + WHERE [OrganizationId] = @OrganizationId + ORDER BY [RevisionDate] DESC +END diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetReportDataById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetReportDataById.sql new file mode 100644 index 0000000000..9905d5aad2 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetReportDataById.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_GetReportDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [ReportData] + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id +END + diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummariesByDateRange.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummariesByDateRange.sql new file mode 100644 index 0000000000..2ab78a2a1e --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummariesByDateRange.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_GetSummariesByDateRange] + @OrganizationId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + SELECT + [SummaryData] + FROM [dbo].[OrganizationReportView] + WHERE [OrganizationId] = @OrganizationId + AND [RevisionDate] >= @StartDate + AND [RevisionDate] <= @EndDate + ORDER BY [RevisionDate] DESC +END + diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummaryDataById.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummaryDataById.sql new file mode 100644 index 0000000000..ff0023c95b --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_GetSummaryDataById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_GetSummaryDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [SummaryData] + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id +END + + diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql deleted file mode 100644 index 6bdcf51f70..0000000000 --- a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_ReadByOrganizationId.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE PROCEDURE [dbo].[OrganizationReport_ReadByOrganizationId] - @OrganizationId UNIQUEIDENTIFIER -AS - SET NOCOUNT ON; - - SELECT - * - FROM [dbo].[OrganizationReportView] - WHERE [OrganizationId] = @OrganizationId; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Update.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Update.sql new file mode 100644 index 0000000000..4732fb8ef4 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_Update.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX), + @SummaryData NVARCHAR(MAX), + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + UPDATE [dbo].[OrganizationReport] + SET + [OrganizationId] = @OrganizationId, + [ReportData] = @ReportData, + [CreationDate] = @CreationDate, + [ContentEncryptionKey] = @ContentEncryptionKey, + [SummaryData] = @SummaryData, + [ApplicationData] = @ApplicationData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id; +END; diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateApplicationData.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateApplicationData.sql new file mode 100644 index 0000000000..573622a5e0 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateApplicationData.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_UpdateApplicationData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationReport] + SET + [ApplicationData] = @ApplicationData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateReportData.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateReportData.sql new file mode 100644 index 0000000000..d7172e100e --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateReportData.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_UpdateReportData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationReport] + SET + [ReportData] = @ReportData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END diff --git a/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateSummaryData.sql b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateSummaryData.sql new file mode 100644 index 0000000000..f33f5980e8 --- /dev/null +++ b/src/Sql/dbo/Dirt/Stored Procedures/OrganizationReport_UpdateSummaryData.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationReport_UpdateSummaryData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @SummaryData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationReport] + SET + [SummaryData] = @SummaryData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END diff --git a/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql index edc7ff4c92..4c47eafad8 100644 --- a/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql +++ b/src/Sql/dbo/Dirt/Tables/OrganizationReport.sql @@ -1,19 +1,24 @@ CREATE TABLE [dbo].[OrganizationReport] ( [Id] UNIQUEIDENTIFIER NOT NULL, [OrganizationId] UNIQUEIDENTIFIER NOT NULL, - [Date] DATETIME2 (7) NOT NULL, [ReportData] NVARCHAR(MAX) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, [ContentEncryptionKey] VARCHAR(MAX) NOT NULL, + [SummaryData] NVARCHAR(MAX) NULL, + [ApplicationData] NVARCHAR(MAX) NULL, + [RevisionDate] DATETIME2 (7) NULL, CONSTRAINT [PK_OrganizationReport] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_OrganizationReport_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) -); + ); GO + CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId] - ON [dbo].[OrganizationReport]([OrganizationId] ASC); + ON [dbo].[OrganizationReport] ([OrganizationId] ASC); GO -CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_Date] - ON [dbo].[OrganizationReport]([OrganizationId] ASC, [Date] DESC); + +CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_RevisionDate] + ON [dbo].[OrganizationReport]([OrganizationId] ASC, [RevisionDate] DESC); GO + diff --git a/test/Api.Test/Dirt/OrganizationReportsControllerTests.cs b/test/Api.Test/Dirt/OrganizationReportsControllerTests.cs new file mode 100644 index 0000000000..c786fd1c1b --- /dev/null +++ b/test/Api.Test/Dirt/OrganizationReportsControllerTests.cs @@ -0,0 +1,1165 @@ +using Bit.Api.Dirt.Controllers; +using Bit.Core.Context; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Dirt; + +[ControllerCustomize(typeof(OrganizationReportsController))] +[SutProviderCustomize] +public class OrganizationReportControllerTests +{ + #region Whole OrganizationReport Endpoints + + [Theory, BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithValidOrgId_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + OrganizationReport expectedReport) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetLatestOrganizationReportAsync(orgId) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetLatestOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(Task.FromResult(false)); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetLatestOrganizationReportAsync(orgId)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetLatestOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetLatestOrganizationReportAsync_WhenNoReportFound_ReturnsOkWithNull( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetLatestOrganizationReportAsync(orgId) + .Returns((OrganizationReport)null); + + // Act + var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Null(okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetLatestOrganizationReportAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + OrganizationReport expectedReport) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetLatestOrganizationReportAsync(orgId) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.GetLatestOrganizationReportAsync(orgId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .GetLatestOrganizationReportAsync(orgId); + } + + + + + [Theory, BitAutoData] + public async Task GetOrganizationReportAsync_WithValidIds_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReport expectedReport) + { + // Arrange + expectedReport.OrganizationId = orgId; + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportAsync(reportId) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(Task.FromResult(false)); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportAsync_WhenReportNotFound_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportAsync(reportId) + .Returns((OrganizationReport)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId)); + + Assert.Equal("Report not found for the specified organization.", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReport expectedReport) + { + // Arrange + expectedReport.OrganizationId = orgId; + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportAsync(reportId) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationReportAsync(reportId); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportAsync_WithValidAccess_UsesCorrectReportId( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReport expectedReport) + { + // Arrange + expectedReport.OrganizationId = orgId; + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportAsync(reportId) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.GetOrganizationReportAsync(orgId, reportId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationReportAsync(reportId); + } + + [Theory, BitAutoData] + public async Task CreateOrganizationReportAsync_WithValidRequest_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + AddOrganizationReportRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .AddOrganizationReportAsync(request) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.CreateOrganizationReportAsync(orgId, request); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task CreateOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + AddOrganizationReportRequest request) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateOrganizationReportAsync(orgId, request)); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .AddOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateOrganizationReportAsync_WithMismatchedOrgId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + AddOrganizationReportRequest request) + { + // Arrange + request.OrganizationId = Guid.NewGuid(); // Different from orgId + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateOrganizationReportAsync(orgId, request)); + + Assert.Equal("Organization ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .AddOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateOrganizationReportAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + AddOrganizationReportRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .AddOrganizationReportAsync(request) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.CreateOrganizationReportAsync(orgId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .AddOrganizationReportAsync(request); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportAsync_WithValidRequest_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + UpdateOrganizationReportRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportAsync(request) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + UpdateOrganizationReportRequest request) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request)); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportAsync_WithMismatchedOrgId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + UpdateOrganizationReportRequest request) + { + // Arrange + request.OrganizationId = Guid.NewGuid(); // Different from orgId + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request)); + + Assert.Equal("Organization ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + UpdateOrganizationReportRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportAsync(request) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.UpdateOrganizationReportAsync(orgId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .UpdateOrganizationReportAsync(request); + } + + #endregion + + #region SummaryData Field Endpoints + + [Theory, BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParameters_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + DateTime startDate, + DateTime endDate, + List expectedSummaryData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate) + .Returns(expectedSummaryData); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedSummaryData, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + DateTime startDate, + DateTime endDate) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationReportSummaryDataByDateRangeAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + DateTime startDate, + DateTime endDate, + List expectedSummaryData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate) + .Returns(expectedSummaryData); + + // Act + await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationReportSummaryDataByDateRangeAsync(orgId, startDate, endDate); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportSummaryAsync_WithValidIds_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReportSummaryDataResponse expectedSummaryData) + { + // Arrange + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportSummaryDataAsync(orgId, reportId) + .Returns(expectedSummaryData); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportSummaryAsync(orgId, reportId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedSummaryData, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportSummaryAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportSummaryAsync(orgId, reportId)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationReportSummaryDataAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportSummaryRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportSummaryAsync(request) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportSummaryRequest request) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request)); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportSummaryAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrgId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportSummaryRequest request) + { + // Arrange + request.OrganizationId = Guid.NewGuid(); // Different from orgId + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request)); + + Assert.Equal("Organization ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportSummaryAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedReportId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportSummaryRequest request) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = Guid.NewGuid(); // Different from reportId + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request)); + + Assert.Equal("Report ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportSummaryAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportSummaryRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportSummaryAsync(request) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(orgId, reportId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .UpdateOrganizationReportSummaryAsync(request); + } + + #endregion + + #region ReportData Field Endpoints + + [Theory, BitAutoData] + public async Task GetOrganizationReportDataAsync_WithValidIds_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReportDataResponse expectedReportData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportDataAsync(orgId, reportId) + .Returns(expectedReportData); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReportData, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportDataAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationReportDataAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportDataAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReportDataResponse expectedReportData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportDataAsync(orgId, reportId) + .Returns(expectedReportData); + + // Act + await sutProvider.Sut.GetOrganizationReportDataAsync(orgId, reportId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationReportDataAsync(orgId, reportId); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportDataRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportDataAsync(request) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportDataRequest request) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request)); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportDataAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrgId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportDataRequest request) + { + // Arrange + request.OrganizationId = Guid.NewGuid(); // Different from orgId + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request)); + + Assert.Equal("Organization ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportDataAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithMismatchedReportId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportDataRequest request) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = Guid.NewGuid(); // Different from reportId + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request)); + + Assert.Equal("Report ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportDataAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportDataAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportDataRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.ReportId = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportDataAsync(request) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.UpdateOrganizationReportDataAsync(orgId, reportId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .UpdateOrganizationReportDataAsync(request); + } + + #endregion + + #region ApplicationData Field Endpoints + + [Theory, BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WithValidIds_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReportApplicationDataResponse expectedApplicationData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportApplicationDataAsync(orgId, reportId) + .Returns(expectedApplicationData); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedApplicationData, okResult.Value); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId)); + + // Verify that the query was not called + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationReportApplicationDataAsync(Arg.Any(), Arg.Any()); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WhenApplicationDataNotFound_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportApplicationDataAsync(orgId, reportId) + .Returns((OrganizationReportApplicationDataResponse)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId)); + + Assert.Equal("Organization report application data not found.", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + OrganizationReportApplicationDataResponse expectedApplicationData) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .GetOrganizationReportApplicationDataAsync(orgId, reportId) + .Returns(expectedApplicationData); + + // Act + await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(orgId, reportId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationReportApplicationDataAsync(orgId, reportId); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ReturnsOkResult( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportApplicationDataRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.Id = reportId; + expectedReport.Id = request.Id; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportApplicationDataAsync(request) + .Returns(expectedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request); + + // Assert + var okResult = Assert.IsType(result); + Assert.Equal(expectedReport, okResult.Value); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithoutAccess_ThrowsNotFoundException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportApplicationDataRequest request) + { + // Arrange + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(false); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request)); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportApplicationDataAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrgId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportApplicationDataRequest request) + { + // Arrange + request.OrganizationId = Guid.NewGuid(); // Different from orgId + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request)); + + Assert.Equal("Organization ID in the request body must match the route parameter", exception.Message); + + // Verify that the command was not called + await sutProvider.GetDependency() + .DidNotReceive() + .UpdateOrganizationReportApplicationDataAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedReportId_ThrowsBadRequestException( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportApplicationDataRequest request, + OrganizationReport updatedReport) + { + // Arrange + request.OrganizationId = orgId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportApplicationDataAsync(request) + .Returns(updatedReport); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request)); + + Assert.Equal("Report ID in the request body must match the route parameter", exception.Message); + } + + [Theory, BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_CallsCorrectMethods( + SutProvider sutProvider, + Guid orgId, + Guid reportId, + UpdateOrganizationReportApplicationDataRequest request, + OrganizationReport expectedReport) + { + // Arrange + request.OrganizationId = orgId; + request.Id = reportId; + expectedReport.Id = reportId; + + sutProvider.GetDependency() + .AccessReports(orgId) + .Returns(true); + + sutProvider.GetDependency() + .UpdateOrganizationReportApplicationDataAsync(request) + .Returns(expectedReport); + + // Act + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(orgId, reportId, request); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .AccessReports(orgId); + + await sutProvider.GetDependency() + .Received(1) + .UpdateOrganizationReportApplicationDataAsync(request); + } + + #endregion +} diff --git a/test/Api.Test/Dirt/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs index 4636406df5..37a6cb79c3 100644 --- a/test/Api.Test/Dirt/ReportsControllerTests.cs +++ b/test/Api.Test/Dirt/ReportsControllerTests.cs @@ -1,14 +1,12 @@ using AutoFixture; using Bit.Api.Dirt.Controllers; using Bit.Api.Dirt.Models; -using Bit.Api.Dirt.Models.Response; using Bit.Core.Context; using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces; using Bit.Core.Dirt.Reports.ReportFeatures.Requests; using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; @@ -144,323 +142,4 @@ public class ReportsControllerTests _.OrganizationId == request.OrganizationId && _.PasswordHealthReportApplicationIds == request.PasswordHealthReportApplicationIds)); } - - [Theory, BitAutoData] - public async Task AddOrganizationReportAsync_withAccess_success(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); - - // Act - var request = new AddOrganizationReportRequest - { - OrganizationId = Guid.NewGuid(), - ReportData = "Report Data", - Date = DateTime.UtcNow - }; - await sutProvider.Sut.AddOrganizationReport(request); - - // Assert - _ = sutProvider.GetDependency() - .Received(1) - .AddOrganizationReportAsync(Arg.Is(_ => - _.OrganizationId == request.OrganizationId && - _.ReportData == request.ReportData && - _.Date == request.Date)); - } - - [Theory, BitAutoData] - public async Task AddOrganizationReportAsync_withoutAccess(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); - // Act - var request = new AddOrganizationReportRequest - { - OrganizationId = Guid.NewGuid(), - ReportData = "Report Data", - Date = DateTime.UtcNow - }; - await Assert.ThrowsAsync(async () => - await sutProvider.Sut.AddOrganizationReport(request)); - // Assert - _ = sutProvider.GetDependency() - .Received(0); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withAccess_success(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); - // Act - var request = new DropOrganizationReportRequest - { - OrganizationId = Guid.NewGuid(), - OrganizationReportIds = new List { Guid.NewGuid(), Guid.NewGuid() } - }; - await sutProvider.Sut.DropOrganizationReport(request); - // Assert - _ = sutProvider.GetDependency() - .Received(1) - .DropOrganizationReportAsync(Arg.Is(_ => - _.OrganizationId == request.OrganizationId && - _.OrganizationReportIds.SequenceEqual(request.OrganizationReportIds))); - } - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withoutAccess(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); - // Act - var request = new DropOrganizationReportRequest - { - OrganizationId = Guid.NewGuid(), - OrganizationReportIds = new List { Guid.NewGuid(), Guid.NewGuid() } - }; - await Assert.ThrowsAsync(async () => - await sutProvider.Sut.DropOrganizationReport(request)); - // Assert - _ = sutProvider.GetDependency() - .Received(0); - } - [Theory, BitAutoData] - public async Task GetOrganizationReportAsync_withAccess_success(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); - // Act - var orgId = Guid.NewGuid(); - var result = await sutProvider.Sut.GetOrganizationReports(orgId); - // Assert - _ = sutProvider.GetDependency() - .Received(1) - .GetOrganizationReportAsync(Arg.Is(_ => _ == orgId)); - } - - [Theory, BitAutoData] - public async Task GetOrganizationReportAsync_withoutAccess(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); - // Act - var orgId = Guid.NewGuid(); - await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReports(orgId)); - // Assert - _ = sutProvider.GetDependency() - .Received(0); - - } - - [Theory, BitAutoData] - public async Task GetLastestOrganizationReportAsync_withAccess_success(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(true); - - // Act - var orgId = Guid.NewGuid(); - var result = await sutProvider.Sut.GetLatestOrganizationReport(orgId); - - // Assert - _ = sutProvider.GetDependency() - .Received(1) - .GetLatestOrganizationReportAsync(Arg.Is(_ => _ == orgId)); - } - - [Theory, BitAutoData] - public async Task GetLastestOrganizationReportAsync_withoutAccess(SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency().AccessReports(Arg.Any()).Returns(false); - - // Act - var orgId = Guid.NewGuid(); - await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetLatestOrganizationReport(orgId)); - - // Assert - _ = sutProvider.GetDependency() - .Received(0); - } - - [Theory, BitAutoData] - public void CreateOrganizationReportSummary_ReturnsNoContent_WhenAccessGranted(SutProvider sutProvider) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key", - Date = DateTime.UtcNow - }; - sutProvider.GetDependency().AccessReports(orgId).Returns(true); - - // Act - var result = sutProvider.Sut.CreateOrganizationReportSummary(model); - - // Assert - Assert.IsType(result); - } - - [Theory, BitAutoData] - public void CreateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied(SutProvider sutProvider) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key", - Date = DateTime.UtcNow - }; - sutProvider.GetDependency().AccessReports(orgId).Returns(false); - - // Act & Assert - Assert.Throws( - () => sutProvider.Sut.CreateOrganizationReportSummary(model)); - } - - [Theory, BitAutoData] - public void GetOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - - sutProvider.GetDependency().AccessReports(orgId).Returns(false); - - // Act & Assert - Assert.Throws( - () => sutProvider.Sut.GetOrganizationReportSummary(orgId, DateOnly.FromDateTime(DateTime.UtcNow), DateOnly.FromDateTime(DateTime.UtcNow))); - } - - [Theory, BitAutoData] - public void GetOrganizationReportSummary_returnsExpectedResult( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var dates = new[] - { - DateOnly.FromDateTime(DateTime.UtcNow), - DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(-1)) - }; - - sutProvider.GetDependency().AccessReports(orgId).Returns(true); - - // Act - var result = sutProvider.Sut.GetOrganizationReportSummary(orgId, dates[0], dates[1]); - - // Assert - Assert.NotNull(result); - } - - [Theory, BitAutoData] - public void CreateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key" - }; - sutProvider.Sut.ModelState.Clear(); - sutProvider.GetDependency().AccessReports(orgId).Returns(true); - - // Act - var result = sutProvider.Sut.CreateOrganizationReportSummary(model); - - // Assert - Assert.IsType(result); - } - - [Theory, BitAutoData] - public void CreateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key" - }; - sutProvider.Sut.ModelState.AddModelError("key", "error"); - - // Act & Assert - Assert.Throws(() => sutProvider.Sut.CreateOrganizationReportSummary(model)); - } - - [Theory, BitAutoData] - public void UpdateOrganizationReportSummary_ReturnsNoContent_WhenModelIsValidAndAccessGranted( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key" - }; - sutProvider.Sut.ModelState.Clear(); - sutProvider.GetDependency().AccessReports(orgId).Returns(true); - - // Act - var result = sutProvider.Sut.UpdateOrganizationReportSummary(model); - - // Assert - Assert.IsType(result); - } - - [Theory, BitAutoData] - public void UpdateOrganizationReportSummary_ThrowsBadRequestException_WhenModelStateIsInvalid( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key" - }; - sutProvider.Sut.ModelState.AddModelError("key", "error"); - - // Act & Assert - Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); - } - - [Theory, BitAutoData] - public void UpdateOrganizationReportSummary_ThrowsNotFoundException_WhenAccessDenied( - SutProvider sutProvider - ) - { - // Arrange - var orgId = Guid.NewGuid(); - var model = new OrganizationReportSummaryModel - { - OrganizationId = orgId, - EncryptedData = "mock-data", - EncryptionKey = "mock-key" - }; - sutProvider.Sut.ModelState.Clear(); - sutProvider.GetDependency().AccessReports(orgId).Returns(false); - - // Act & Assert - Assert.Throws(() => sutProvider.Sut.UpdateOrganizationReportSummary(model)); - } } diff --git a/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs deleted file mode 100644 index f6a5c13be9..0000000000 --- a/test/Core.Test/Dirt/ReportFeatures/DeleteOrganizationReportCommandTests.cs +++ /dev/null @@ -1,194 +0,0 @@ -using AutoFixture; -using Bit.Core.Dirt.Entities; -using Bit.Core.Dirt.Reports.ReportFeatures; -using Bit.Core.Dirt.Reports.ReportFeatures.Requests; -using Bit.Core.Dirt.Repositories; -using Bit.Core.Exceptions; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Dirt.ReportFeatures; - -[SutProviderCustomize] -public class DeleteOrganizationReportCommandTests -{ - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withValidRequest_Success( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var OrganizationReports = fixture.CreateMany(2).ToList(); - // only take one id from the list - we only want to drop one record - var request = fixture.Build() - .With(x => x.OrganizationReportIds, - OrganizationReports.Select(x => x.Id).Take(1).ToList()) - .Create(); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(OrganizationReports); - - // Act - await sutProvider.Sut.DropOrganizationReportAsync(request); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .GetByOrganizationIdAsync(request.OrganizationId); - - await sutProvider.GetDependency() - .Received(1) - .DeleteAsync(Arg.Is(_ => - request.OrganizationReportIds.Contains(_.Id))); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withValidRequest_nothingToDrop( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var OrganizationReports = fixture.CreateMany(2).ToList(); - // we are passing invalid data - var request = fixture.Build() - .With(x => x.OrganizationReportIds, new List { Guid.NewGuid() }) - .Create(); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(OrganizationReports); - - // Act - await sutProvider.Sut.DropOrganizationReportAsync(request); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .GetByOrganizationIdAsync(request.OrganizationId); - - await sutProvider.GetDependency() - .Received(0) - .DeleteAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withNodata_fails( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - // we are passing invalid data - var request = fixture.Build() - .Create(); - - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(null as List); - - // Act - await Assert.ThrowsAsync(() => - sutProvider.Sut.DropOrganizationReportAsync(request)); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .GetByOrganizationIdAsync(request.OrganizationId); - - await sutProvider.GetDependency() - .Received(0) - .DeleteAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withInvalidOrganizationId_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var request = fixture.Create(); - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(null as List); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withInvalidOrganizationReportId_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var request = fixture.Create(); - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(new List()); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withNullOrganizationId_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var request = fixture.Build() - .With(x => x.OrganizationId, default(Guid)) - .Create(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withNullOrganizationReportIds_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var request = fixture.Build() - .With(x => x.OrganizationReportIds, default(List)) - .Create(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withEmptyOrganizationReportIds_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var request = fixture.Build() - .With(x => x.OrganizationReportIds, new List()) - .Create(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - - [Theory, BitAutoData] - public async Task DropOrganizationReportAsync_withEmptyRequest_ShouldThrowError( - SutProvider sutProvider) - { - // Arrange - var request = new DropOrganizationReportRequest(); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.DropOrganizationReportAsync(request)); - Assert.Equal("No data found.", exception.Message); - } - -} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportApplicationDataQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportApplicationDataQueryTests.cs new file mode 100644 index 0000000000..c9281d52d1 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportApplicationDataQueryTests.cs @@ -0,0 +1,116 @@ +using AutoFixture; +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class GetOrganizationReportApplicationDataQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WithValidParams_ShouldReturnApplicationData( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + var reportId = fixture.Create(); + var applicationDataResponse = fixture.Build() + .Create(); + + sutProvider.GetDependency() + .GetApplicationDataAsync(reportId) + .Returns(applicationDataResponse); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId); + + // Assert + Assert.NotNull(result); + await sutProvider.GetDependency() + .Received(1).GetApplicationDataAsync(reportId); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var reportId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(Guid.Empty, reportId)); + + Assert.Equal("OrganizationId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetApplicationDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, Guid.Empty)); + + Assert.Equal("ReportId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetApplicationDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WhenDataNotFound_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + + sutProvider.GetDependency() + .GetApplicationDataAsync(reportId) + .Returns((OrganizationReportApplicationDataResponse)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId)); + + Assert.Equal("Organization report application data not found.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var expectedMessage = "Database connection failed"; + + sutProvider.GetDependency() + .GetApplicationDataAsync(reportId) + .Throws(new InvalidOperationException(expectedMessage)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportApplicationDataAsync(organizationId, reportId)); + + Assert.Equal(expectedMessage, exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportDataQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportDataQueryTests.cs new file mode 100644 index 0000000000..3c00c6870a --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportDataQueryTests.cs @@ -0,0 +1,116 @@ +using AutoFixture; +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class GetOrganizationReportDataQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetOrganizationReportDataAsync_WithValidParams_ShouldReturnReportData( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + var reportId = fixture.Create(); + var reportDataResponse = fixture.Build() + .Create(); + + sutProvider.GetDependency() + .GetReportDataAsync(reportId) + .Returns(reportDataResponse); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId); + + // Assert + Assert.NotNull(result); + await sutProvider.GetDependency() + .Received(1).GetReportDataAsync(reportId); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var reportId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportDataAsync(Guid.Empty, reportId)); + + Assert.Equal("OrganizationId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetReportDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, Guid.Empty)); + + Assert.Equal("ReportId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetReportDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportDataAsync_WhenDataNotFound_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + + sutProvider.GetDependency() + .GetReportDataAsync(reportId) + .Returns((OrganizationReportDataResponse)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId)); + + Assert.Equal("Organization report data not found.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var expectedMessage = "Database connection failed"; + + sutProvider.GetDependency() + .GetReportDataAsync(reportId) + .Throws(new InvalidOperationException(expectedMessage)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportDataAsync(organizationId, reportId)); + + Assert.Equal(expectedMessage, exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs deleted file mode 100644 index 19d020be12..0000000000 --- a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportQueryTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -using AutoFixture; -using Bit.Core.Dirt.Entities; -using Bit.Core.Dirt.Reports.ReportFeatures; -using Bit.Core.Dirt.Repositories; -using Bit.Core.Exceptions; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Dirt.ReportFeatures; - -[SutProviderCustomize] -public class GetOrganizationReportQueryTests -{ - [Theory] - [BitAutoData] - public async Task GetOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = fixture.Create(); - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(fixture.CreateMany(2).ToList()); - - // Act - var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId); - - // Assert - Assert.NotNull(result); - Assert.True(result.Count() == 2); - } - - [Theory] - [BitAutoData] - public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Is(x => x == Guid.Empty)) - .Returns(new List()); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task GetLatestOrganizationReportAsync_WithValidOrganizationId_ShouldReturnOrganizationReport( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = fixture.Create(); - sutProvider.GetDependency() - .GetLatestByOrganizationIdAsync(Arg.Any()) - .Returns(fixture.Create()); - - // Act - var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId); - - // Assert - Assert.NotNull(result); - } - - [Theory] - [BitAutoData] - public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldFail( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - sutProvider.GetDependency() - .GetLatestByOrganizationIdAsync(Arg.Is(x => x == Guid.Empty)) - .Returns(default(OrganizationReport)); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(Guid.Empty)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } - - [Theory] - [BitAutoData] - public async Task GetOrganizationReportAsync_WithNoReports_ShouldReturnEmptyList( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = fixture.Create(); - sutProvider.GetDependency() - .GetByOrganizationIdAsync(Arg.Any()) - .Returns(new List()); - - // Act - var result = await sutProvider.Sut.GetOrganizationReportAsync(organizationId); - - // Assert - Assert.NotNull(result); - Assert.Empty(result); - } - [Theory] - [BitAutoData] - public async Task GetLatestOrganizationReportAsync_WithNoReports_ShouldReturnNull( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = fixture.Create(); - sutProvider.GetDependency() - .GetLatestByOrganizationIdAsync(Arg.Any()) - .Returns(default(OrganizationReport)); - - // Act - var result = await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId); - - // Assert - Assert.Null(result); - } - [Theory] - [BitAutoData] - public async Task GetOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = default(Guid); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } - [Theory] - [BitAutoData] - public async Task GetLatestOrganizationReportAsync_WithNullOrganizationId_ShouldThrowException( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = default(Guid); - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } - [Theory] - [BitAutoData] - public async Task GetOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = Guid.Empty; - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetOrganizationReportAsync(organizationId)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } - [Theory] - [BitAutoData] - public async Task GetLatestOrganizationReportAsync_WithInvalidOrganizationId_ShouldThrowException( - SutProvider sutProvider) - { - // Arrange - var fixture = new Fixture(); - var organizationId = Guid.Empty; - - // Act & Assert - var exception = await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetLatestOrganizationReportAsync(organizationId)); - - // Assert - Assert.Equal("OrganizationId is required.", exception.Message); - } -} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQueryTests.cs new file mode 100644 index 0000000000..572b7e21fb --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataByDateRangeQueryTests.cs @@ -0,0 +1,133 @@ +using AutoFixture; +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class GetOrganizationReportSummaryDataByDateRangeQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithValidParams_ShouldReturnSummaryData( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + var reportId = fixture.Create(); + var startDate = DateTime.UtcNow.AddDays(-30); + var endDate = DateTime.UtcNow; + var summaryDataList = fixture.Build() + .CreateMany(3); + + sutProvider.GetDependency() + .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate) + .Returns(summaryDataList); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate); + + // Assert + Assert.NotNull(result); + Assert.Equal(3, result.Count()); + await sutProvider.GetDependency() + .Received(1).GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var reportId = Guid.NewGuid(); + var startDate = DateTime.UtcNow.AddDays(-30); + var endDate = DateTime.UtcNow; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(Guid.Empty, startDate, endDate)); + + Assert.Equal("OrganizationId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive() + .GetSummaryDataByDateRangeAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithStartDateAfterEndDate_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var startDate = DateTime.UtcNow; + var endDate = DateTime.UtcNow.AddDays(-30); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate)); + + Assert.Equal("StartDate must be earlier than or equal to EndDate", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetSummaryDataByDateRangeAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WithEmptyResult_ShouldReturnEmptyList( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var startDate = DateTime.UtcNow.AddDays(-30); + var endDate = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate) + .Returns(new List()); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataByDateRangeAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var startDate = DateTime.UtcNow.AddDays(-30); + var endDate = DateTime.UtcNow; + var expectedMessage = "Database connection failed"; + + sutProvider.GetDependency() + .GetSummaryDataByDateRangeAsync(organizationId, startDate, endDate) + .Throws(new InvalidOperationException(expectedMessage)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate)); + + Assert.Equal(expectedMessage, exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataQueryTests.cs new file mode 100644 index 0000000000..c6ede1fcab --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/GetOrganizationReportSummaryDataQueryTests.cs @@ -0,0 +1,116 @@ +using AutoFixture; +using Bit.Core.Dirt.Models.Data; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class GetOrganizationReportSummaryDataQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataAsync_WithValidParams_ShouldReturnSummaryData( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var organizationId = fixture.Create(); + var reportId = fixture.Create(); + var summaryDataResponse = fixture.Build() + .Create(); + + sutProvider.GetDependency() + .GetSummaryDataAsync(reportId) + .Returns(summaryDataResponse); + + // Act + var result = await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId); + + // Assert + Assert.NotNull(result); + await sutProvider.GetDependency() + .Received(1).GetSummaryDataAsync(reportId); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var reportId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(Guid.Empty, reportId)); + + Assert.Equal("OrganizationId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetSummaryDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, Guid.Empty)); + + Assert.Equal("ReportId is required.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().GetSummaryDataAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataAsync_WhenDataNotFound_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + + sutProvider.GetDependency() + .GetSummaryDataAsync(reportId) + .Returns((OrganizationReportSummaryDataResponse)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId)); + + Assert.Equal("Organization report summary data not found.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationReportSummaryDataAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var organizationId = Guid.NewGuid(); + var reportId = Guid.NewGuid(); + var expectedMessage = "Database connection failed"; + + sutProvider.GetDependency() + .GetSummaryDataAsync(reportId) + .Throws(new InvalidOperationException(expectedMessage)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.GetOrganizationReportSummaryDataAsync(organizationId, reportId)); + + Assert.Equal(expectedMessage, exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportApplicationDataCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportApplicationDataCommandTests.cs new file mode 100644 index 0000000000..bd6eee79d9 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportApplicationDataCommandTests.cs @@ -0,0 +1,252 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class UpdateOrganizationReportApplicationDataCommandTests +{ + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithValidRequest_ShouldReturnUpdatedReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.Id, Guid.NewGuid()) + .With(x => x.OrganizationId, Guid.NewGuid()) + .With(x => x.ApplicationData, "updated application data") + .Create(); + + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.Id) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + var updatedReport = fixture.Build() + .With(x => x.Id, request.Id) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.Id) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData) + .Returns(updatedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(updatedReport.Id, result.Id); + Assert.Equal(updatedReport.OrganizationId, result.OrganizationId); + await sutProvider.GetDependency() + .Received(1).UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("OrganizationId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateApplicationDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.Id, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Id is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateApplicationDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns((Organization)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithEmptyApplicationData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ApplicationData, string.Empty) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Application Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithNullApplicationData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ApplicationData, (string)null) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Application Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithNonExistentReport_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.Id) + .Returns((OrganizationReport)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Organization report not found", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.Id) + .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.Id) + .Returns(existingReport); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Organization report does not belong to the specified organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportApplicationDataAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.Id) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.Id) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateApplicationDataAsync(request.OrganizationId, request.Id, request.ApplicationData) + .Throws(new InvalidOperationException("Database connection failed")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportApplicationDataAsync(request)); + + Assert.Equal("Database connection failed", exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportCommandTests.cs new file mode 100644 index 0000000000..3a84eb0d80 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportCommandTests.cs @@ -0,0 +1,230 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class UpdateOrganizationReportCommandTests +{ + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithValidRequest_ShouldReturnUpdatedReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.NewGuid()) + .With(x => x.OrganizationId, Guid.NewGuid()) + .With(x => x.ReportData, "updated report data") + .Create(); + + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + var updatedReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .With(x => x.ReportData, request.ReportData) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + sutProvider.GetDependency() + .UpsertAsync(Arg.Any()) + .Returns(Task.CompletedTask); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport, updatedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(updatedReport.Id, result.Id); + Assert.Equal(updatedReport.OrganizationId, result.OrganizationId); + Assert.Equal(updatedReport.ReportData, result.ReportData); + + await sutProvider.GetDependency() + .Received(1).GetByIdAsync(request.OrganizationId); + await sutProvider.GetDependency() + .Received(2).GetByIdAsync(request.ReportId); + await sutProvider.GetDependency() + .Received(1).UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("OrganizationId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("ReportId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithInvalidOrganization_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns((Organization)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithEmptyReportData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportData, string.Empty) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithNullReportData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportData, (string)null) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithNonExistentReport_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns((OrganizationReport)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("Organization report not found", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportAsync(request)); + + Assert.Equal("Organization report does not belong to the specified organization", exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportDataCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportDataCommandTests.cs new file mode 100644 index 0000000000..02cd74cbf6 --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportDataCommandTests.cs @@ -0,0 +1,252 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class UpdateOrganizationReportDataCommandTests +{ + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithValidRequest_ShouldReturnUpdatedReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.NewGuid()) + .With(x => x.OrganizationId, Guid.NewGuid()) + .With(x => x.ReportData, "updated report data") + .Create(); + + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + var updatedReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData) + .Returns(updatedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportDataAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(updatedReport.Id, result.Id); + Assert.Equal(updatedReport.OrganizationId, result.OrganizationId); + await sutProvider.GetDependency() + .Received(1).UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("OrganizationId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateReportDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("ReportId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateReportDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithInvalidOrganization_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns((Organization)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithEmptyReportData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportData, string.Empty) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithNullReportData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportData, (string)null) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Report Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithNonExistentReport_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns((OrganizationReport)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Organization report not found", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Organization report does not belong to the specified organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportDataAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateReportDataAsync(request.OrganizationId, request.ReportId, request.ReportData) + .Throws(new InvalidOperationException("Database connection failed")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportDataAsync(request)); + + Assert.Equal("Database connection failed", exception.Message); + } +} diff --git a/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportSummaryCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportSummaryCommandTests.cs new file mode 100644 index 0000000000..dae3ff35ba --- /dev/null +++ b/test/Core.Test/Dirt/ReportFeatures/UpdateOrganizationReportSummaryCommandTests.cs @@ -0,0 +1,252 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Dirt.Entities; +using Bit.Core.Dirt.Reports.ReportFeatures; +using Bit.Core.Dirt.Reports.ReportFeatures.Requests; +using Bit.Core.Dirt.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Dirt.ReportFeatures; + +[SutProviderCustomize] +public class UpdateOrganizationReportSummaryCommandTests +{ + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithValidRequest_ShouldReturnUpdatedReport( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.NewGuid()) + .With(x => x.OrganizationId, Guid.NewGuid()) + .With(x => x.SummaryData, "updated summary data") + .Create(); + + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + var updatedReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData) + .Returns(updatedReport); + + // Act + var result = await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(updatedReport.Id, result.Id); + Assert.Equal(updatedReport.OrganizationId, result.OrganizationId); + await sutProvider.GetDependency() + .Received(1).UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithEmptyOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.OrganizationId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("OrganizationId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateSummaryDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithEmptyReportId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.ReportId, Guid.Empty) + .Create(); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("ReportId is required", exception.Message); + await sutProvider.GetDependency() + .DidNotReceive().UpdateSummaryDataAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithInvalidOrganization_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns((Organization)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Invalid Organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithEmptySummaryData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.SummaryData, string.Empty) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Summary Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithNullSummaryData_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Build() + .With(x => x.SummaryData, (string)null) + .Create(); + + var organization = fixture.Create(); + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Summary Data is required", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithNonExistentReport_ShouldThrowNotFoundException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns((OrganizationReport)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Organization report not found", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WithMismatchedOrganizationId_ShouldThrowBadRequestException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, Guid.NewGuid()) // Different org ID + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Organization report does not belong to the specified organization", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateOrganizationReportSummaryAsync_WhenRepositoryThrowsException_ShouldPropagateException( + SutProvider sutProvider) + { + // Arrange + var fixture = new Fixture(); + var request = fixture.Create(); + var organization = fixture.Create(); + var existingReport = fixture.Build() + .With(x => x.Id, request.ReportId) + .With(x => x.OrganizationId, request.OrganizationId) + .Create(); + + sutProvider.GetDependency() + .GetByIdAsync(request.OrganizationId) + .Returns(organization); + sutProvider.GetDependency() + .GetByIdAsync(request.ReportId) + .Returns(existingReport); + sutProvider.GetDependency() + .UpdateSummaryDataAsync(request.OrganizationId, request.ReportId, request.SummaryData) + .Throws(new InvalidOperationException("Database connection failed")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.UpdateOrganizationReportSummaryAsync(request)); + + Assert.Equal("Database connection failed", exception.Message); + } +} diff --git a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs index dd2adc0970..abf16a56e6 100644 --- a/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Dirt/Repositories/OrganizationReportRepositoryTests.cs @@ -42,8 +42,8 @@ public class OrganizationReportRepositoryTests var sqlOrganization = await sqlOrganizationRepo.CreateAsync(organization); report.OrganizationId = sqlOrganization.Id; - var sqlOrgnizationReportRecord = await sqlOrganizationReportRepo.CreateAsync(report); - var savedSqlOrganizationReport = await sqlOrganizationReportRepo.GetByIdAsync(sqlOrgnizationReportRecord.Id); + var sqlOrganizationReportRecord = await sqlOrganizationReportRepo.CreateAsync(report); + var savedSqlOrganizationReport = await sqlOrganizationReportRepo.GetByIdAsync(sqlOrganizationReportRecord.Id); records.Add(savedSqlOrganizationReport); Assert.True(records.Count == 4); @@ -51,17 +51,19 @@ public class OrganizationReportRepositoryTests [CiSkippedTheory, EfOrganizationReportAutoData] public async Task RetrieveByOrganisation_Works( - OrganizationReportRepository sqlPasswordHealthReportApplicationRepo, + OrganizationReportRepository sqlOrganizationReportRepo, SqlRepo.OrganizationRepository sqlOrganizationRepo) { - var (firstOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); - var (secondOrg, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlPasswordHealthReportApplicationRepo); + var (firstOrg, firstReport) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var (secondOrg, secondReport) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); - var firstSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(firstOrg.Id); - var nextSetOfRecords = await sqlPasswordHealthReportApplicationRepo.GetByOrganizationIdAsync(secondOrg.Id); + var firstRetrievedReport = await sqlOrganizationReportRepo.GetByIdAsync(firstReport.Id); + var secondRetrievedReport = await sqlOrganizationReportRepo.GetByIdAsync(secondReport.Id); - Assert.True(firstSetOfRecords.Count == 1 && firstSetOfRecords.First().OrganizationId == firstOrg.Id); - Assert.True(nextSetOfRecords.Count == 1 && nextSetOfRecords.First().OrganizationId == secondOrg.Id); + Assert.NotNull(firstRetrievedReport); + Assert.NotNull(secondRetrievedReport); + Assert.Equal(firstOrg.Id, firstRetrievedReport.OrganizationId); + Assert.Equal(secondOrg.Id, secondRetrievedReport.OrganizationId); } [CiSkippedTheory, EfOrganizationReportAutoData] @@ -112,6 +114,251 @@ public class OrganizationReportRepositoryTests Assert.True(dbRecords.Where(_ => _ == null).Count() == 4); } + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetLatestByOrganizationIdAsync_ShouldReturnLatestReport( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, firstReport) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + + // Create a second report for the same organization with a later revision date + var fixture = new Fixture(); + var secondReport = fixture.Build() + .With(x => x.OrganizationId, org.Id) + .With(x => x.RevisionDate, firstReport.RevisionDate.AddMinutes(30)) + .Create(); + + await sqlOrganizationReportRepo.CreateAsync(secondReport); + + // Act + var latestReport = await sqlOrganizationReportRepo.GetLatestByOrganizationIdAsync(org.Id); + + // Assert + Assert.NotNull(latestReport); + Assert.Equal(org.Id, latestReport.OrganizationId); + Assert.True(latestReport.RevisionDate >= firstReport.RevisionDate); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task UpdateSummaryDataAsync_ShouldUpdateSummaryAndRevisionDate( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (_, report) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + report.RevisionDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)); // ensure old revision date + var newSummaryData = "Updated summary data"; + var originalRevisionDate = report.RevisionDate; + + // Act + var updatedReport = await sqlOrganizationReportRepo.UpdateSummaryDataAsync(report.OrganizationId, report.Id, newSummaryData); + + // Assert + Assert.NotNull(updatedReport); + Assert.Equal(newSummaryData, updatedReport.SummaryData); + Assert.True(updatedReport.RevisionDate > originalRevisionDate); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetSummaryDataAsync_ShouldReturnSummaryData( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var summaryData = "Test summary data"; + var (org, report) = await CreateOrganizationAndReportWithSummaryDataAsync( + sqlOrganizationRepo, sqlOrganizationReportRepo, summaryData); + + // Act + var result = await sqlOrganizationReportRepo.GetSummaryDataAsync(report.Id); + + // Assert + Assert.NotNull(result); + Assert.Equal(summaryData, result.SummaryData); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetSummaryDataByDateRangeAsync_ShouldReturnFilteredResults( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var baseDate = DateTime.UtcNow; + var startDate = baseDate.AddDays(-10); + var endDate = baseDate.AddDays(1); + + // Create organization first + var fixture = new Fixture(); + var organization = fixture.Create(); + var org = await sqlOrganizationRepo.CreateAsync(organization); + + // Create first report with a date within range + var report1 = fixture.Build() + .With(x => x.OrganizationId, org.Id) + .With(x => x.SummaryData, "Summary 1") + .With(x => x.CreationDate, baseDate.AddDays(-5)) // Within range + .With(x => x.RevisionDate, baseDate.AddDays(-5)) + .Create(); + await sqlOrganizationReportRepo.CreateAsync(report1); + + // Create second report with a date within range + var report2 = fixture.Build() + .With(x => x.OrganizationId, org.Id) + .With(x => x.SummaryData, "Summary 2") + .With(x => x.CreationDate, baseDate.AddDays(-3)) // Within range + .With(x => x.RevisionDate, baseDate.AddDays(-3)) + .Create(); + await sqlOrganizationReportRepo.CreateAsync(report2); + + // Act + var results = await sqlOrganizationReportRepo.GetSummaryDataByDateRangeAsync( + org.Id, startDate, endDate); + + // Assert + Assert.NotNull(results); + var resultsList = results.ToList(); + Assert.True(resultsList.Count >= 2, $"Expected at least 2 results, but got {resultsList.Count}"); + Assert.All(resultsList, r => Assert.NotNull(r.SummaryData)); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetReportDataAsync_ShouldReturnReportData( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var fixture = new Fixture(); + var reportData = "Test report data"; + var (org, report) = await CreateOrganizationAndReportWithReportDataAsync( + sqlOrganizationRepo, sqlOrganizationReportRepo, reportData); + + // Act + var result = await sqlOrganizationReportRepo.GetReportDataAsync(report.Id); + + // Assert + Assert.NotNull(result); + Assert.Equal(reportData, result.ReportData); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task UpdateReportDataAsync_ShouldUpdateReportDataAndRevisionDate( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, report) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var newReportData = "Updated report data"; + var originalRevisionDate = report.RevisionDate; + + // Add a small delay to ensure revision date difference + await Task.Delay(100); + + // Act + var updatedReport = await sqlOrganizationReportRepo.UpdateReportDataAsync( + org.Id, report.Id, newReportData); + + // Assert + Assert.NotNull(updatedReport); + Assert.Equal(org.Id, updatedReport.OrganizationId); + Assert.Equal(report.Id, updatedReport.Id); + Assert.Equal(newReportData, updatedReport.ReportData); + Assert.True(updatedReport.RevisionDate >= originalRevisionDate, + $"Expected RevisionDate {updatedReport.RevisionDate} to be >= {originalRevisionDate}"); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetApplicationDataAsync_ShouldReturnApplicationData( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var applicationData = "Test application data"; + var (org, report) = await CreateOrganizationAndReportWithApplicationDataAsync( + sqlOrganizationRepo, sqlOrganizationReportRepo, applicationData); + + // Act + var result = await sqlOrganizationReportRepo.GetApplicationDataAsync(report.Id); + + // Assert + Assert.NotNull(result); + Assert.Equal(applicationData, result.ApplicationData); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task UpdateApplicationDataAsync_ShouldUpdateApplicationDataAndRevisionDate( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, report) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var newApplicationData = "Updated application data"; + var originalRevisionDate = DateTime.UtcNow.Subtract(TimeSpan.FromDays(1)); // ensure old revision date + + // Add a small delay to ensure revision date difference + await Task.Delay(100); + + // Act + var updatedReport = await sqlOrganizationReportRepo.UpdateApplicationDataAsync( + org.Id, report.Id, newApplicationData); + + // Assert + Assert.NotNull(updatedReport); + Assert.Equal(org.Id, updatedReport.OrganizationId); + Assert.Equal(report.Id, updatedReport.Id); + Assert.Equal(newApplicationData, updatedReport.ApplicationData); + Assert.True(updatedReport.RevisionDate >= originalRevisionDate, + $"Expected RevisionDate {updatedReport.RevisionDate} to be >= {originalRevisionDate}"); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetSummaryDataAsync_WithNonExistentReport_ShouldReturnNull( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var nonExistentReportId = Guid.NewGuid(); + + // Act + var result = await sqlOrganizationReportRepo.GetSummaryDataAsync(nonExistentReportId); + + // Assert + Assert.Null(result); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetReportDataAsync_WithNonExistentReport_ShouldReturnNull( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var nonExistentReportId = Guid.NewGuid(); + + // Act + var result = await sqlOrganizationReportRepo.GetReportDataAsync(nonExistentReportId); + + // Assert + Assert.Null(result); + } + + [CiSkippedTheory, EfOrganizationReportAutoData] + public async Task GetApplicationDataAsync_WithNonExistentReport_ShouldReturnNull( + OrganizationReportRepository sqlOrganizationReportRepo, + SqlRepo.OrganizationRepository sqlOrganizationRepo) + { + // Arrange + var (org, _) = await CreateOrganizationAndReportAsync(sqlOrganizationRepo, sqlOrganizationReportRepo); + var nonExistentReportId = Guid.NewGuid(); + + // Act + var result = await sqlOrganizationReportRepo.GetApplicationDataAsync(nonExistentReportId); + + // Assert + Assert.Null(result); + } + private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportAsync( IOrganizationRepository orgRepo, IOrganizationReportRepository orgReportRepo) @@ -121,6 +368,64 @@ public class OrganizationReportRepositoryTests var orgReportRecord = fixture.Build() .With(x => x.OrganizationId, organization.Id) + .With(x => x.RevisionDate, organization.RevisionDate) + .Create(); + + organization = await orgRepo.CreateAsync(organization); + orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord); + + return (organization, orgReportRecord); + } + + private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportWithSummaryDataAsync( + IOrganizationRepository orgRepo, + IOrganizationReportRepository orgReportRepo, + string summaryData) + { + var fixture = new Fixture(); + var organization = fixture.Create(); + + var orgReportRecord = fixture.Build() + .With(x => x.OrganizationId, organization.Id) + .With(x => x.SummaryData, summaryData) + .Create(); + + organization = await orgRepo.CreateAsync(organization); + orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord); + + return (organization, orgReportRecord); + } + + private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportWithReportDataAsync( + IOrganizationRepository orgRepo, + IOrganizationReportRepository orgReportRepo, + string reportData) + { + var fixture = new Fixture(); + var organization = fixture.Create(); + + var orgReportRecord = fixture.Build() + .With(x => x.OrganizationId, organization.Id) + .With(x => x.ReportData, reportData) + .Create(); + + organization = await orgRepo.CreateAsync(organization); + orgReportRecord = await orgReportRepo.CreateAsync(orgReportRecord); + + return (organization, orgReportRecord); + } + + private async Task<(Organization, OrganizationReport)> CreateOrganizationAndReportWithApplicationDataAsync( + IOrganizationRepository orgRepo, + IOrganizationReportRepository orgReportRepo, + string applicationData) + { + var fixture = new Fixture(); + var organization = fixture.Create(); + + var orgReportRecord = fixture.Build() + .With(x => x.OrganizationId, organization.Id) + .With(x => x.ApplicationData, applicationData) .Create(); organization = await orgRepo.CreateAsync(organization); diff --git a/util/Migrator/DbScripts/2025-08-22_00_AlterOrganizationReport.sql b/util/Migrator/DbScripts/2025-08-22_00_AlterOrganizationReport.sql new file mode 100644 index 0000000000..912fe0de46 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-22_00_AlterOrganizationReport.sql @@ -0,0 +1,96 @@ +IF EXISTS ( +SELECT * FROM sys.indexes WHERE name = 'IX_OrganizationReport_OrganizationId_Date' +AND object_id = OBJECT_ID('dbo.OrganizationReport') +) +BEGIN + DROP INDEX [IX_OrganizationReport_OrganizationId_Date] ON [dbo].[OrganizationReport]; +END +GO + +IF COL_LENGTH('[dbo].[OrganizationReport]', 'Date') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[OrganizationReport] + DROP COLUMN [Date]; +END +GO + +IF OBJECT_ID('dbo.OrganizationReport') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[OrganizationReport] + ADD [SummaryData] NVARCHAR(MAX) NULL, + [ApplicationData] NVARCHAR(MAX) NULL, + [RevisionDate] DATETIME2 (7) NULL; +END +GO + +IF NOT EXISTS ( +SELECT * FROM sys.indexes WHERE name = 'IX_OrganizationReport_OrganizationId_RevisionDate' +AND object_id = OBJECT_ID('dbo.OrganizationReport') +) +BEGIN + CREATE NONCLUSTERED INDEX [IX_OrganizationReport_OrganizationId_RevisionDate] + ON [dbo].[OrganizationReport]([OrganizationId] ASC, [RevisionDate] DESC); +END +GO + +IF OBJECT_ID('dbo.OrganizationReportView') IS NOT NULL +BEGIN + DROP VIEW [dbo].[OrganizationReportView]; +END +GO + +IF OBJECT_ID('dbo.OrganizationReportView') IS NULL +BEGIN + EXEC('CREATE VIEW [dbo].[OrganizationReportView] + AS + SELECT + * + FROM + [dbo].[OrganizationReport]'); +END +GO + +IF OBJECT_ID('dbo.OrganizationReport_Create') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationReport_Create]; +END +GO + +IF OBJECT_ID('dbo.OrganizationReport_Create') IS NULL +BEGIN + EXEC('CREATE PROCEDURE [dbo].[OrganizationReport_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX), + @SummaryData NVARCHAR(MAX), + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) + AS + BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[OrganizationReport]( + [Id], + [OrganizationId], + [ReportData], + [CreationDate], + [ContentEncryptionKey], + [SummaryData], + [ApplicationData], + [RevisionDate] + ) + VALUES ( + @Id, + @OrganizationId, + @ReportData, + @CreationDate, + @ContentEncryptionKey, + @SummaryData, + @ApplicationData, + @RevisionDate + ); + END'); +END +GO diff --git a/util/Migrator/DbScripts/2025-08-22_01_AddOrganizationReportStoredProcedures.sql b/util/Migrator/DbScripts/2025-08-22_01_AddOrganizationReportStoredProcedures.sql new file mode 100644 index 0000000000..6f64a3ee6a --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-22_01_AddOrganizationReportStoredProcedures.sql @@ -0,0 +1,156 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_GetLatestByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT TOP 1 + [Id], + [OrganizationId], + [ReportData], + [CreationDate], + [ContentEncryptionKey], + [SummaryData], + [ApplicationData], + [RevisionDate] + FROM [dbo].[OrganizationReportView] + WHERE [OrganizationId] = @OrganizationId + ORDER BY [RevisionDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_GetSummariesByDateRange] + @OrganizationId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + +SELECT + [SummaryData] +FROM [dbo].[OrganizationReportView] +WHERE [OrganizationId] = @OrganizationId + AND [RevisionDate] >= @StartDate + AND [RevisionDate] <= @EndDate +ORDER BY [RevisionDate] DESC +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_GetSummaryDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + [SummaryData] +FROM [dbo].[OrganizationReportView] +WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_UpdateSummaryData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @SummaryData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + +UPDATE [dbo].[OrganizationReport] +SET + [SummaryData] = @SummaryData, + [RevisionDate] = @RevisionDate +WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_GetReportDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [ReportData] + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_UpdateReportData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + +UPDATE [dbo].[OrganizationReport] +SET + [ReportData] = @ReportData, + [RevisionDate] = @RevisionDate +WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_GetApplicationDataById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [ApplicationData] + FROM [dbo].[OrganizationReportView] + WHERE [Id] = @Id; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_UpdateApplicationData] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + + UPDATE [dbo].[OrganizationReport] + SET + [ApplicationData] = @ApplicationData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id + AND [OrganizationId] = @OrganizationId; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationReport_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ReportData NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @ContentEncryptionKey VARCHAR(MAX), + @SummaryData NVARCHAR(MAX), + @ApplicationData NVARCHAR(MAX), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON; + UPDATE [dbo].[OrganizationReport] + SET + [OrganizationId] = @OrganizationId, + [ReportData] = @ReportData, + [CreationDate] = @CreationDate, + [ContentEncryptionKey] = @ContentEncryptionKey, + [SummaryData] = @SummaryData, + [ApplicationData] = @ApplicationData, + [RevisionDate] = @RevisionDate + WHERE [Id] = @Id; +END +GO diff --git a/util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.Designer.cs b/util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.Designer.cs new file mode 100644 index 0000000000..2a75cf0ed9 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.Designer.cs @@ -0,0 +1,3275 @@ +// +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("20250825064449_2025-08-22_00_AlterOrganizationReport")] + partial class _20250822_00_AlterOrganizationReport + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250825064449_2025-08-22_00_AlterOrganizationReport.cs b/util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.cs new file mode 100644 index 0000000000..3dcb753f34 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250825064449_2025-08-22_00_AlterOrganizationReport.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250822_00_AlterOrganizationReport : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Date", + table: "OrganizationReport", + newName: "RevisionDate"); + + migrationBuilder.AddColumn( + name: "ApplicationData", + table: "OrganizationReport", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "SummaryData", + table: "OrganizationReport", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApplicationData", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "SummaryData", + table: "OrganizationReport"); + + migrationBuilder.RenameColumn( + name: "RevisionDate", + table: "OrganizationReport", + newName: "Date"); + } +} diff --git a/util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs b/util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs new file mode 100644 index 0000000000..c94c03b193 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs @@ -0,0 +1,3275 @@ +// +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("20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures")] + partial class _20250822_01_AddOrganizationReportStoredProcedures + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.cs b/util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.cs new file mode 100644 index 0000000000..e9aaf605f3 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250829194438_2025-08-22_01_AddOrganizationReportStoredProcedures.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250822_01_AddOrganizationReportStoredProcedures : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 2500cc3623..69301d7e54 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1011,6 +1011,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Id") .HasColumnType("char(36)"); + b.Property("ApplicationData") + .HasColumnType("longtext"); + b.Property("ContentEncryptionKey") .IsRequired() .HasColumnType("longtext"); @@ -1018,9 +1021,6 @@ namespace Bit.MySqlMigrations.Migrations b.Property("CreationDate") .HasColumnType("datetime(6)"); - b.Property("Date") - .HasColumnType("datetime(6)"); - b.Property("OrganizationId") .HasColumnType("char(36)"); @@ -1028,6 +1028,12 @@ namespace Bit.MySqlMigrations.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + b.HasKey("Id"); b.HasIndex("Id") diff --git a/util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.Designer.cs b/util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.Designer.cs new file mode 100644 index 0000000000..cc45046c33 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.Designer.cs @@ -0,0 +1,3281 @@ +// +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("20250825064440_2025-08-22_00_AlterOrganizationReport")] + partial class _20250822_00_AlterOrganizationReport + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250825064440_2025-08-22_00_AlterOrganizationReport.cs b/util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.cs new file mode 100644 index 0000000000..f5449e8a57 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250825064440_2025-08-22_00_AlterOrganizationReport.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250822_00_AlterOrganizationReport : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Date", + table: "OrganizationReport", + newName: "RevisionDate"); + + migrationBuilder.AddColumn( + name: "ApplicationData", + table: "OrganizationReport", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SummaryData", + table: "OrganizationReport", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApplicationData", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "SummaryData", + table: "OrganizationReport"); + + migrationBuilder.RenameColumn( + name: "RevisionDate", + table: "OrganizationReport", + newName: "Date"); + } +} diff --git a/util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs b/util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs new file mode 100644 index 0000000000..d04e454476 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs @@ -0,0 +1,3281 @@ +// +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("20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures")] + partial class _20250822_01_AddOrganizationReportStoredProcedures + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.cs b/util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.cs new file mode 100644 index 0000000000..605d2ab01a --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250829194432_2025-08-22_01_AddOrganizationReportStoredProcedures.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250822_01_AddOrganizationReportStoredProcedures : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 41f49e6e63..b0e34084e8 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1016,6 +1016,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Id") .HasColumnType("uuid"); + b.Property("ApplicationData") + .HasColumnType("text"); + b.Property("ContentEncryptionKey") .IsRequired() .HasColumnType("text"); @@ -1023,9 +1026,6 @@ namespace Bit.PostgresMigrations.Migrations b.Property("CreationDate") .HasColumnType("timestamp with time zone"); - b.Property("Date") - .HasColumnType("timestamp with time zone"); - b.Property("OrganizationId") .HasColumnType("uuid"); @@ -1033,6 +1033,12 @@ namespace Bit.PostgresMigrations.Migrations .IsRequired() .HasColumnType("text"); + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + b.HasKey("Id"); b.HasIndex("Id") diff --git a/util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.Designer.cs b/util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.Designer.cs new file mode 100644 index 0000000000..6aee5c15f0 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.Designer.cs @@ -0,0 +1,3264 @@ +// +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("20250825064445_2025-08-22_00_AlterOrganizationReport")] + partial class _20250822_00_AlterOrganizationReport + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250825064445_2025-08-22_00_AlterOrganizationReport.cs b/util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.cs new file mode 100644 index 0000000000..32ecf61589 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250825064445_2025-08-22_00_AlterOrganizationReport.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250822_00_AlterOrganizationReport : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Date", + table: "OrganizationReport", + newName: "RevisionDate"); + + migrationBuilder.AddColumn( + name: "ApplicationData", + table: "OrganizationReport", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "SummaryData", + table: "OrganizationReport", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApplicationData", + table: "OrganizationReport"); + + migrationBuilder.DropColumn( + name: "SummaryData", + table: "OrganizationReport"); + + migrationBuilder.RenameColumn( + name: "RevisionDate", + table: "OrganizationReport", + newName: "Date"); + } +} diff --git a/util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs b/util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs new file mode 100644 index 0000000000..e33f825366 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.Designer.cs @@ -0,0 +1,3264 @@ +// +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("20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures")] + partial class _20250822_01_AddOrganizationReportStoredProcedures + { + /// + 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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.cs b/util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.cs new file mode 100644 index 0000000000..a26a75078f --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250829194441_2025-08-22_01_AddOrganizationReportStoredProcedures.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250822_01_AddOrganizationReportStoredProcedures : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 11d1517a05..caee8fef2a 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1000,6 +1000,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Id") .HasColumnType("TEXT"); + b.Property("ApplicationData") + .HasColumnType("TEXT"); + b.Property("ContentEncryptionKey") .IsRequired() .HasColumnType("TEXT"); @@ -1007,9 +1010,6 @@ namespace Bit.SqliteMigrations.Migrations b.Property("CreationDate") .HasColumnType("TEXT"); - b.Property("Date") - .HasColumnType("TEXT"); - b.Property("OrganizationId") .HasColumnType("TEXT"); @@ -1017,6 +1017,12 @@ namespace Bit.SqliteMigrations.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("Id") From d0778a8a7b84f08934f12feee2938296973f5ff2 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:02:10 -0400 Subject: [PATCH 212/326] Clean up OrgnizationIntegrationRequestModel validations and nullable declarations (#6301) * Clean up OrgnizationIntegrationRequestModel validations; remove unnecessary nullable enables * Fix weird line break --- .../OrgnizationIntegrationRequestModel.cs | 57 ++++++++----------- .../Data/EventIntegrations/HecIntegration.cs | 4 +- .../EventIntegrations/IIntegrationMessage.cs | 4 +- .../IntegrationFilterGroup.cs | 4 +- .../IntegrationFilterOperation.cs | 3 +- .../IntegrationFilterRule.cs | 4 +- .../IntegrationHandlerResult.cs | 4 +- .../EventIntegrations/IntegrationMessage.cs | 4 +- .../IntegrationTemplateContext.cs | 4 +- .../EventIntegrations/SlackIntegration.cs | 4 +- .../SlackIntegrationConfiguration.cs | 4 +- .../SlackIntegrationConfigurationDetails.cs | 4 +- .../EventIntegrations/WebhookIntegration.cs | 4 +- .../WebhookIntegrationConfiguration.cs | 4 +- .../WebhookIntegrationConfigurationDetails.cs | 4 +- .../AzureServiceBusEventListenerService.cs | 4 +- ...ureServiceBusIntegrationListenerService.cs | 4 +- .../EventIntegrationEventWriteService.cs | 4 +- .../EventIntegrationHandler.cs | 4 +- .../EventRepositoryHandler.cs | 4 +- .../EventIntegrations/EventRouteService.cs | 4 +- .../IntegrationFilterFactory.cs | 4 +- .../IntegrationFilterService.cs | 4 +- .../RabbitMqEventListenerService.cs | 4 +- .../RabbitMqIntegrationListenerService.cs | 4 +- .../EventIntegrations/RabbitMqService.cs | 4 +- .../SlackIntegrationHandler.cs | 4 +- .../EventIntegrations/SlackService.cs | 4 +- .../WebhookIntegrationHandler.cs | 4 +- ...rganizationIntegrationRequestModelTests.cs | 10 ++-- 30 files changed, 58 insertions(+), 120 deletions(-) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs index 5fa2e86a90..92d65ab8fe 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs @@ -8,9 +8,9 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations; public class OrganizationIntegrationRequestModel : IValidatableObject { - public string? Configuration { get; set; } + public string? Configuration { get; init; } - public IntegrationType Type { get; set; } + public IntegrationType Type { get; init; } public OrganizationIntegration ToOrganizationIntegration(Guid organizationId) { @@ -33,62 +33,55 @@ public class OrganizationIntegrationRequestModel : IValidatableObject switch (Type) { case IntegrationType.CloudBillingSync or IntegrationType.Scim: - yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", new[] { nameof(Type) }); + yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", [nameof(Type)]); break; case IntegrationType.Slack: - yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) }); + yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", [nameof(Type)]); break; case IntegrationType.Webhook: - if (string.IsNullOrWhiteSpace(Configuration)) - { - break; - } - if (!IsIntegrationValid()) - { - yield return new ValidationResult( - "Webhook integrations must include valid configuration.", - new[] { nameof(Configuration) }); - } + foreach (var r in ValidateConfiguration(allowNullOrEmpty: true)) + yield return r; break; case IntegrationType.Hec: - if (!IsIntegrationValid()) - { - yield return new ValidationResult( - "HEC integrations must include valid configuration.", - new[] { nameof(Configuration) }); - } + foreach (var r in ValidateConfiguration(allowNullOrEmpty: false)) + yield return r; break; case IntegrationType.Datadog: - if (!IsIntegrationValid()) - { - yield return new ValidationResult( - "Datadog integrations must include valid configuration.", - new[] { nameof(Configuration) }); - } + foreach (var r in ValidateConfiguration(allowNullOrEmpty: false)) + yield return r; break; default: yield return new ValidationResult( $"Integration type '{Type}' is not recognized.", - new[] { nameof(Type) }); + [nameof(Type)]); break; } } - private bool IsIntegrationValid() + private List ValidateConfiguration(bool allowNullOrEmpty) { + var results = new List(); + if (string.IsNullOrWhiteSpace(Configuration)) { - return false; + if (!allowNullOrEmpty) + results.Add(InvalidConfig()); + return results; } try { - var config = JsonSerializer.Deserialize(Configuration); - return config is not null; + if (JsonSerializer.Deserialize(Configuration) is null) + results.Add(InvalidConfig()); } catch { - return false; + results.Add(InvalidConfig()); } + + return results; } + + private static ValidationResult InvalidConfig() => + new(errorMessage: $"Must include valid {typeof(T).Name} configuration.", memberNames: [nameof(Configuration)]); } diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs index eff9f8e1be..33ae5dadbe 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/HecIntegration.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs index f979b8af0e..7a0962d89a 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IIntegrationMessage.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.Enums; +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs index bb0c2e01ba..276ca3a14b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterGroup.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationFilterGroup { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs index f09df47738..fddf630e26 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterOperation.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public enum IntegrationFilterOperation { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs index b9d90a0442..b5f90f5e63 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationFilterRule.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationFilterRule { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs index d3b0c0d5ac..8db054561b 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationHandlerResult.cs @@ -1,6 +1,4 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public class IntegrationHandlerResult { diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs index 1861ec4522..11a5229f8c 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationMessage.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index 82c236865f..266c810470 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs index e8bfaee303..dc2733c889 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegration(string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs index 2c757aeb76..5b4fae0c76 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs index 6c3d4c2fff..d22f43bb92 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record SlackIntegrationConfigurationDetails(string ChannelId, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs index 84b4b97857..dcda4caa92 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegration.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 2f5e8d29c1..851bd3f411 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index 4fa1a67c8e..dba9b1714d 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -1,5 +1,3 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; +namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs index f5eb41c051..91f8fac888 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusEventListenerService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text; +using System.Text; using Azure.Messaging.ServiceBus; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs index 037ae7e647..e415430965 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/AzureServiceBusIntegrationListenerService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Azure.Messaging.ServiceBus; +using Azure.Messaging.ServiceBus; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs index 519f8aeb32..309b4a8409 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationEventWriteService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.Models.Data; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 9cd789be76..0a8ab67554 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs index 0fab787589..ee3a2d5db2 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRepositoryHandler.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs index df0819b409..a542e75a7b 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventRouteService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs index b90ea8d16e..d28ac910b7 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterFactory.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Linq.Expressions; +using System.Linq.Expressions; using Bit.Core.Models.Data; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs index 88877c329a..1c8fae4000 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/IntegrationFilterService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text.Json; +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Models.Data; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs index 5b089b06a6..430540a2f7 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqEventListenerService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text; +using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Logging; using RabbitMQ.Client; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs index 59c8782985..b426032c92 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqIntegrationListenerService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text; +using System.Text; using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Microsoft.Extensions.Hosting; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs index 20ae31a113..3e20e34200 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/RabbitMqService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Text; +using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Enums; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs index 6f55c0cf9c..2d29494afc 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs index 3f82217830..f17185c4d3 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackService.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Web; using Bit.Core.Models.Slack; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index e0c2b66a90..0599f6e9d4 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Net.Http.Headers; +using System.Net.Http.Headers; using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs index 9565a76822..81927a1bfe 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationRequestModelTests.cs @@ -84,7 +84,7 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must include valid configuration", results[0].ErrorMessage); + Assert.Contains("Must include valid", results[0].ErrorMessage); } [Fact] @@ -114,7 +114,7 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must include valid configuration", results[0].ErrorMessage); + Assert.Contains("Must include valid", results[0].ErrorMessage); } [Fact] @@ -130,7 +130,7 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must include valid configuration", results[0].ErrorMessage); + Assert.Contains("Must include valid", results[0].ErrorMessage); } [Fact] @@ -160,7 +160,7 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must include valid configuration", results[0].ErrorMessage); + Assert.Contains("Must include valid", results[0].ErrorMessage); } [Fact] @@ -176,7 +176,7 @@ public class OrganizationIntegrationRequestModelTests Assert.Single(results); Assert.Contains(nameof(model.Configuration), results[0].MemberNames); - Assert.Contains("must include valid configuration", results[0].ErrorMessage); + Assert.Contains("Must include valid", results[0].ErrorMessage); } [Fact] From ac718351a8317553359bd07cd3b9c8219b2bfb62 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:33:22 +0530 Subject: [PATCH 213/326] Fix UseKeyConnector is set to true when upgrading to Enterprise (#6281) --- src/Api/Billing/Controllers/OrganizationBillingController.cs | 2 +- .../OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 762b06db96..21b17bff67 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -304,7 +304,7 @@ public class OrganizationBillingController( sale.Organization.UsePolicies = plan.HasPolicies; sale.Organization.UseSso = plan.HasSso; sale.Organization.UseResetPassword = plan.HasResetPassword; - sale.Organization.UseKeyConnector = plan.HasKeyConnector; + sale.Organization.UseKeyConnector = plan.HasKeyConnector ? organization.UseKeyConnector : false; sale.Organization.UseScim = plan.HasScim; sale.Organization.UseCustomPermissions = plan.HasCustomPermissions; sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains; diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 6e514bfea7..2b39e6cca6 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -265,7 +265,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; - organization.UseKeyConnector = newPlan.HasKeyConnector; + organization.UseKeyConnector = newPlan.HasKeyConnector ? organization.UseKeyConnector : false; organization.UseScim = newPlan.HasScim; organization.UseResetPassword = newPlan.HasResetPassword; organization.SelfHost = newPlan.HasSelfHost; From 3dd5accb56eec5aaf43fb0541272647edbff0343 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:22:42 -0500 Subject: [PATCH 214/326] [PM-24964] Stripe-hosted bank account verification (#6263) * Implement bank account hosted URL verification with webhook handling notification * Fix tests * Run dotnet format * Remove unused VerifyBankAccount operation * Stephon's feedback * Removing unused test * TEMP: Add logging for deployment check * Run dotnet format * fix test * Revert "fix test" This reverts commit b8743ab3b57d93eb12754ac586b0bce100834f48. * Revert "Run dotnet format" This reverts commit 5c861b0b72131b954b244639bf3fa1b4a303515e. * Revert "TEMP: Add logging for deployment check" This reverts commit 0a88acd6a1571a9c3a24657c0e199a5fc18e9a50. * Resolve GetPaymentMethodQuery order of operations --- .../Services/ProviderBillingService.cs | 6 +- .../Services/ProviderBillingServiceTests.cs | 6 +- .../OrganizationBillingVNextController.cs | 14 +- .../VNext/ProviderBillingVNextController.cs | 13 +- src/Billing/Constants/HandledStripeWebhook.cs | 1 + .../Services/IPushNotificationAdapter.cs | 11 + src/Billing/Services/IStripeEventService.cs | 46 +- src/Billing/Services/IStripeFacade.cs | 6 + src/Billing/Services/IStripeWebhookHandler.cs | 2 + .../PaymentSucceededHandler.cs | 76 +-- .../PushNotificationAdapter.cs | 71 ++ .../SetupIntentSucceededHandler.cs | 77 +++ .../Implementations/StripeEventProcessor.cs | 84 +-- .../Implementations/StripeEventService.cs | 219 +++--- .../Services/Implementations/StripeFacade.cs | 8 + .../SubscriptionUpdatedHandler.cs | 11 +- src/Billing/Startup.cs | 2 + src/Core/Billing/Caches/ISetupIntentCache.cs | 7 +- .../SetupIntentDistributedCache.cs | 38 +- .../Queries/GetOrganizationWarningsQuery.cs | 2 +- .../Services/OrganizationBillingService.cs | 2 +- .../Commands/VerifyBankAccountCommand.cs | 62 -- .../Payment/Models/MaskedPaymentMethod.cs | 10 +- .../Payment/Queries/GetPaymentMethodQuery.cs | 58 +- src/Core/Billing/Payment/Registrations.cs | 1 - .../PremiumUserBillingService.cs | 2 +- .../Implementations/SubscriberService.cs | 4 +- src/Core/Models/PushNotification.cs | 11 + .../Platform/Push/IPushNotificationService.cs | 14 - src/Core/Platform/Push/PushType.cs | 12 +- src/Notifications/HubHelpers.cs | 15 + .../SetupIntentSucceededHandlerTests.cs | 242 +++++++ .../Services/StripeEventServiceTests.cs | 637 +++++++++++------- .../SubscriptionUpdatedHandlerTests.cs | 13 +- .../GetOrganizationWarningsQueryTests.cs | 4 +- .../UpdatePaymentMethodCommandTests.cs | 21 +- .../Commands/VerifyBankAccountCommandTests.cs | 81 --- .../Models/MaskedPaymentMethodTests.cs | 4 +- .../Queries/GetPaymentMethodQueryTests.cs | 13 +- .../Services/SubscriberServiceTests.cs | 4 +- .../Push/Engines/AzureQueuePushEngineTests.cs | 25 - .../Platform/Push/Engines/PushTestBase.cs | 15 - 42 files changed, 1136 insertions(+), 814 deletions(-) create mode 100644 src/Billing/Services/IPushNotificationAdapter.cs create mode 100644 src/Billing/Services/Implementations/PushNotificationAdapter.cs create mode 100644 src/Billing/Services/Implementations/SetupIntentSucceededHandler.cs delete mode 100644 src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs create mode 100644 test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs delete mode 100644 test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs 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 5169d6cfd1..398674c7b6 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -636,10 +636,10 @@ public class ProviderBillingService( { case PaymentMethodType.BankAccount: { - var setupIntentId = await setupIntentCache.Get(provider.Id); + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id); await stripeAdapter.SetupIntentCancel(setupIntentId, new SetupIntentCancelOptions { CancellationReason = "abandoned" }); - await setupIntentCache.Remove(provider.Id); + await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id); break; } case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): @@ -689,7 +689,7 @@ public class ProviderBillingService( }); } - var setupIntentId = await setupIntentCache.Get(provider.Id); + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id); var setupIntent = !string.IsNullOrEmpty(setupIntentId) ? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 4e811017f9..54c0b82aa9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -1003,7 +1003,7 @@ public class ProviderBillingServiceTests o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) .Throws(); - sutProvider.GetDependency().Get(provider.Id).Returns("setup_intent_id"); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id"); await Assert.ThrowsAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); @@ -1013,7 +1013,7 @@ public class ProviderBillingServiceTests await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is(options => options.CancellationReason == "abandoned")); - await sutProvider.GetDependency().Received(1).Remove(provider.Id); + await sutProvider.GetDependency().Received(1).RemoveSetupIntentForSubscriber(provider.Id); } [Theory, BitAutoData] @@ -1644,7 +1644,7 @@ public class ProviderBillingServiceTests const string setupIntentId = "seti_123"; - sutProvider.GetDependency().Get(provider.Id).Returns(setupIntentId); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId); sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is(options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index a85dfe11e1..ee98031dbc 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -25,8 +25,7 @@ public class OrganizationBillingVNextController( IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IUpdateBillingAddressCommand updateBillingAddressCommand, - IUpdatePaymentMethodCommand updatePaymentMethodCommand, - IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController + IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController { [Authorize] [HttpGet("address")] @@ -96,17 +95,6 @@ public class OrganizationBillingVNextController( return Handle(result); } - [Authorize] - [HttpPost("payment-method/verify-bank-account")] - [InjectOrganization] - public async Task VerifyBankAccountAsync( - [BindNever] Organization organization, - [FromBody] VerifyBankAccountRequest request) - { - var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode); - return Handle(result); - } - [Authorize] [HttpGet("warnings")] [InjectOrganization] diff --git a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs index b0b39eaf4a..0ea9bad682 100644 --- a/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/ProviderBillingVNextController.cs @@ -23,8 +23,7 @@ public class ProviderBillingVNextController( IGetProviderWarningsQuery getProviderWarningsQuery, IProviderService providerService, IUpdateBillingAddressCommand updateBillingAddressCommand, - IUpdatePaymentMethodCommand updatePaymentMethodCommand, - IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController + IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController { [HttpGet("address")] [InjectProvider(ProviderUserType.ProviderAdmin)] @@ -97,16 +96,6 @@ public class ProviderBillingVNextController( return Handle(result); } - [HttpPost("payment-method/verify-bank-account")] - [InjectProvider(ProviderUserType.ProviderAdmin)] - public async Task VerifyBankAccountAsync( - [BindNever] Provider provider, - [FromBody] VerifyBankAccountRequest request) - { - var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode); - return Handle(result); - } - [HttpGet("warnings")] [InjectProvider(ProviderUserType.ServiceUser)] public async Task GetWarningsAsync( diff --git a/src/Billing/Constants/HandledStripeWebhook.cs b/src/Billing/Constants/HandledStripeWebhook.cs index cbcc2065c3..e9e0c5a16b 100644 --- a/src/Billing/Constants/HandledStripeWebhook.cs +++ b/src/Billing/Constants/HandledStripeWebhook.cs @@ -13,4 +13,5 @@ public static class HandledStripeWebhook public const string PaymentMethodAttached = "payment_method.attached"; public const string CustomerUpdated = "customer.updated"; public const string InvoiceFinalized = "invoice.finalized"; + public const string SetupIntentSucceeded = "setup_intent.succeeded"; } diff --git a/src/Billing/Services/IPushNotificationAdapter.cs b/src/Billing/Services/IPushNotificationAdapter.cs new file mode 100644 index 0000000000..2f74f35eec --- /dev/null +++ b/src/Billing/Services/IPushNotificationAdapter.cs @@ -0,0 +1,11 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; + +namespace Bit.Billing.Services; + +public interface IPushNotificationAdapter +{ + Task NotifyBankAccountVerifiedAsync(Organization organization); + Task NotifyBankAccountVerifiedAsync(Provider provider); + Task NotifyEnabledChangedAsync(Organization organization); +} diff --git a/src/Billing/Services/IStripeEventService.cs b/src/Billing/Services/IStripeEventService.cs index bf242905ee..567d404ba6 100644 --- a/src/Billing/Services/IStripeEventService.cs +++ b/src/Billing/Services/IStripeEventService.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Stripe; +using Stripe; namespace Bit.Billing.Services; @@ -13,12 +10,10 @@ public interface IStripeEventService /// and optionally expands it with the provided options. /// /// The Stripe webhook event. - /// Determines whether or not to retrieve a fresh copy of the charge object from Stripe. + /// Determines whether to retrieve a fresh copy of the charge object from Stripe. /// Optionally provided to expand the fresh charge object retrieved from Stripe. /// A Stripe . - /// Thrown when the Stripe event does not contain a charge object. - /// Thrown when is true and Stripe's API returns a null charge object. - Task GetCharge(Event stripeEvent, bool fresh = false, List expand = null); + Task GetCharge(Event stripeEvent, bool fresh = false, List? expand = null); /// /// Extracts the object from the Stripe . When is true, @@ -26,12 +21,10 @@ public interface IStripeEventService /// and optionally expands it with the provided options. /// /// The Stripe webhook event. - /// Determines whether or not to retrieve a fresh copy of the customer object from Stripe. + /// Determines whether to retrieve a fresh copy of the customer object from Stripe. /// Optionally provided to expand the fresh customer object retrieved from Stripe. /// A Stripe . - /// Thrown when the Stripe event does not contain a customer object. - /// Thrown when is true and Stripe's API returns a null customer object. - Task GetCustomer(Event stripeEvent, bool fresh = false, List expand = null); + Task GetCustomer(Event stripeEvent, bool fresh = false, List? expand = null); /// /// Extracts the object from the Stripe . When is true, @@ -39,12 +32,10 @@ public interface IStripeEventService /// and optionally expands it with the provided options. /// /// The Stripe webhook event. - /// Determines whether or not to retrieve a fresh copy of the invoice object from Stripe. + /// Determines whether to retrieve a fresh copy of the invoice object from Stripe. /// Optionally provided to expand the fresh invoice object retrieved from Stripe. /// A Stripe . - /// Thrown when the Stripe event does not contain an invoice object. - /// Thrown when is true and Stripe's API returns a null invoice object. - Task GetInvoice(Event stripeEvent, bool fresh = false, List expand = null); + Task GetInvoice(Event stripeEvent, bool fresh = false, List? expand = null); /// /// Extracts the object from the Stripe . When is true, @@ -52,12 +43,21 @@ public interface IStripeEventService /// and optionally expands it with the provided options. /// /// The Stripe webhook event. - /// Determines whether or not to retrieve a fresh copy of the payment method object from Stripe. + /// Determines whether to retrieve a fresh copy of the payment method object from Stripe. /// Optionally provided to expand the fresh payment method object retrieved from Stripe. /// A Stripe . - /// Thrown when the Stripe event does not contain an payment method object. - /// Thrown when is true and Stripe's API returns a null payment method object. - Task GetPaymentMethod(Event stripeEvent, bool fresh = false, List expand = null); + Task GetPaymentMethod(Event stripeEvent, bool fresh = false, List? expand = null); + + /// + /// Extracts the object from the Stripe . When is true, + /// uses the setup intent ID extracted from the event to retrieve the most up-to-update setup intent from Stripe's API + /// and optionally expands it with the provided options. + /// + /// The Stripe webhook event. + /// Determines whether to retrieve a fresh copy of the setup intent object from Stripe. + /// Optionally provided to expand the fresh setup intent object retrieved from Stripe. + /// A Stripe . + Task GetSetupIntent(Event stripeEvent, bool fresh = false, List? expand = null); /// /// Extracts the object from the Stripe . When is true, @@ -65,12 +65,10 @@ public interface IStripeEventService /// and optionally expands it with the provided options. /// /// The Stripe webhook event. - /// Determines whether or not to retrieve a fresh copy of the subscription object from Stripe. + /// Determines whether to retrieve a fresh copy of the subscription object from Stripe. /// Optionally provided to expand the fresh subscription object retrieved from Stripe. /// A Stripe . - /// Thrown when the Stripe event does not contain an subscription object. - /// Thrown when is true and Stripe's API returns a null subscription object. - Task GetSubscription(Event stripeEvent, bool fresh = false, List expand = null); + Task GetSubscription(Event stripeEvent, bool fresh = false, List? expand = null); /// /// Ensures that the customer associated with the Stripe is in the correct region for this server. diff --git a/src/Billing/Services/IStripeFacade.cs b/src/Billing/Services/IStripeFacade.cs index 37ba51cc61..280a3aca3c 100644 --- a/src/Billing/Services/IStripeFacade.cs +++ b/src/Billing/Services/IStripeFacade.cs @@ -38,6 +38,12 @@ public interface IStripeFacade RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task GetSetupIntent( + string setupIntentId, + SetupIntentGetOptions setupIntentGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default); + Task> ListInvoices( InvoiceListOptions options = null, RequestOptions requestOptions = null, diff --git a/src/Billing/Services/IStripeWebhookHandler.cs b/src/Billing/Services/IStripeWebhookHandler.cs index 59be435489..2619b2f663 100644 --- a/src/Billing/Services/IStripeWebhookHandler.cs +++ b/src/Billing/Services/IStripeWebhookHandler.cs @@ -65,3 +65,5 @@ public interface ICustomerUpdatedHandler : IStripeWebhookHandler; /// Defines the contract for handling Stripe Invoice Finalized events. /// public interface IInvoiceFinalizedHandler : IStripeWebhookHandler; + +public interface ISetupIntentSucceededHandler : IStripeWebhookHandler; diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index 4c256e3d85..a10fa4b3d6 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -3,63 +3,38 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; -public class PaymentSucceededHandler : IPaymentSucceededHandler +public class PaymentSucceededHandler( + ILogger logger, + IStripeEventService stripeEventService, + IStripeFacade stripeFacade, + IProviderRepository providerRepository, + IOrganizationRepository organizationRepository, + IStripeEventUtilityService stripeEventUtilityService, + IUserService userService, + IOrganizationEnableCommand organizationEnableCommand, + IPricingClient pricingClient, + IPushNotificationAdapter pushNotificationAdapter) + : IPaymentSucceededHandler { - private readonly ILogger _logger; - private readonly IStripeEventService _stripeEventService; - private readonly IUserService _userService; - private readonly IStripeFacade _stripeFacade; - private readonly IProviderRepository _providerRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IStripeEventUtilityService _stripeEventUtilityService; - private readonly IPushNotificationService _pushNotificationService; - private readonly IOrganizationEnableCommand _organizationEnableCommand; - private readonly IPricingClient _pricingClient; - - public PaymentSucceededHandler( - ILogger logger, - IStripeEventService stripeEventService, - IStripeFacade stripeFacade, - IProviderRepository providerRepository, - IOrganizationRepository organizationRepository, - IStripeEventUtilityService stripeEventUtilityService, - IUserService userService, - IPushNotificationService pushNotificationService, - IOrganizationEnableCommand organizationEnableCommand, - IPricingClient pricingClient) - { - _logger = logger; - _stripeEventService = stripeEventService; - _stripeFacade = stripeFacade; - _providerRepository = providerRepository; - _organizationRepository = organizationRepository; - _stripeEventUtilityService = stripeEventUtilityService; - _userService = userService; - _pushNotificationService = pushNotificationService; - _organizationEnableCommand = organizationEnableCommand; - _pricingClient = pricingClient; - } - /// /// Handles the event type from Stripe. /// /// public async Task HandleAsync(Event parsedEvent) { - var invoice = await _stripeEventService.GetInvoice(parsedEvent, true); + var invoice = await stripeEventService.GetInvoice(parsedEvent, true); if (!invoice.Paid || invoice.BillingReason != "subscription_create") { return; } - var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId); + var subscription = await stripeFacade.GetSubscription(invoice.SubscriptionId); if (subscription?.Status != StripeSubscriptionStatus.Active) { return; @@ -70,15 +45,15 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler await Task.Delay(5000); } - var (organizationId, userId, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); + var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); if (providerId.HasValue) { - var provider = await _providerRepository.GetByIdAsync(providerId.Value); + var provider = await providerRepository.GetByIdAsync(providerId.Value); if (provider == null) { - _logger.LogError( + logger.LogError( "Received invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) that does not exist", parsedEvent.Id, providerId.Value); @@ -86,9 +61,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler return; } - var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly); + var teamsMonthly = await pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly); - var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly); + var enterpriseMonthly = await pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly); var teamsMonthlyLineItem = subscription.Items.Data.FirstOrDefault(item => @@ -100,29 +75,30 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler if (teamsMonthlyLineItem == null || enterpriseMonthlyLineItem == null) { - _logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items", + logger.LogError("invoice.payment_succeeded webhook ({EventID}) for Provider ({ProviderID}) indicates missing subscription line items", parsedEvent.Id, provider.Id); } } else if (organizationId.HasValue) { - var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); + var organization = await organizationRepository.GetByIdAsync(organizationId.Value); if (organization == null) { return; } - var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType); + var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id)) { return; } - await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); - await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); + await organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + organization = await organizationRepository.GetByIdAsync(organization.Id); + await pushNotificationAdapter.NotifyEnabledChangedAsync(organization!); } else if (userId.HasValue) { @@ -131,7 +107,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler return; } - await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); + await userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); } } } diff --git a/src/Billing/Services/Implementations/PushNotificationAdapter.cs b/src/Billing/Services/Implementations/PushNotificationAdapter.cs new file mode 100644 index 0000000000..673ae1415e --- /dev/null +++ b/src/Billing/Services/Implementations/PushNotificationAdapter.cs @@ -0,0 +1,71 @@ +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.Enums; +using Bit.Core.Models; +using Bit.Core.Platform.Push; + +namespace Bit.Billing.Services.Implementations; + +public class PushNotificationAdapter( + IProviderUserRepository providerUserRepository, + IPushNotificationService pushNotificationService) : IPushNotificationAdapter +{ + public Task NotifyBankAccountVerifiedAsync(Organization organization) => + pushNotificationService.PushAsync(new PushNotification + { + Type = PushType.OrganizationBankAccountVerified, + Target = NotificationTarget.Organization, + TargetId = organization.Id, + Payload = new OrganizationBankAccountVerifiedPushNotification + { + OrganizationId = organization.Id + }, + ExcludeCurrentContext = false + }); + + public async Task NotifyBankAccountVerifiedAsync(Provider provider) + { + var providerUsers = await providerUserRepository.GetManyByProviderAsync(provider.Id); + var providerAdmins = providerUsers.Where(providerUser => providerUser is + { + Type: ProviderUserType.ProviderAdmin, + Status: ProviderUserStatusType.Confirmed, + UserId: not null + }).ToList(); + + if (providerAdmins.Count > 0) + { + var tasks = providerAdmins.Select(providerAdmin => pushNotificationService.PushAsync( + new PushNotification + { + Type = PushType.ProviderBankAccountVerified, + Target = NotificationTarget.User, + TargetId = providerAdmin.UserId!.Value, + Payload = new ProviderBankAccountVerifiedPushNotification + { + ProviderId = provider.Id, + AdminId = providerAdmin.UserId!.Value + }, + ExcludeCurrentContext = false + })); + + await Task.WhenAll(tasks); + } + } + + public Task NotifyEnabledChangedAsync(Organization organization) => + pushNotificationService.PushAsync(new PushNotification + { + Type = PushType.SyncOrganizationStatusChanged, + Target = NotificationTarget.Organization, + TargetId = organization.Id, + Payload = new OrganizationStatusPushNotification + { + OrganizationId = organization.Id, + Enabled = organization.Enabled, + }, + ExcludeCurrentContext = false, + }); +} diff --git a/src/Billing/Services/Implementations/SetupIntentSucceededHandler.cs b/src/Billing/Services/Implementations/SetupIntentSucceededHandler.cs new file mode 100644 index 0000000000..bc3fa1bd56 --- /dev/null +++ b/src/Billing/Services/Implementations/SetupIntentSucceededHandler.cs @@ -0,0 +1,77 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; +using Bit.Core.Repositories; +using Bit.Core.Services; +using OneOf; +using Stripe; +using Event = Stripe.Event; + +namespace Bit.Billing.Services.Implementations; + +public class SetupIntentSucceededHandler( + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IPushNotificationAdapter pushNotificationAdapter, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + IStripeEventService stripeEventService) : ISetupIntentSucceededHandler +{ + public async Task HandleAsync(Event parsedEvent) + { + var setupIntent = await stripeEventService.GetSetupIntent( + parsedEvent, + true, + ["payment_method"]); + + if (setupIntent is not + { + PaymentMethod.UsBankAccount: not null + }) + { + return; + } + + var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id); + if (subscriberId == null) + { + return; + } + + var organization = await organizationRepository.GetByIdAsync(subscriberId.Value); + var provider = await providerRepository.GetByIdAsync(subscriberId.Value); + + OneOf entity = organization != null ? organization : provider!; + await SetPaymentMethodAsync(entity, setupIntent.PaymentMethod); + } + + private async Task SetPaymentMethodAsync( + OneOf subscriber, + PaymentMethod paymentMethod) + { + var customerId = subscriber.Match( + organization => organization.GatewayCustomerId, + provider => provider.GatewayCustomerId); + + if (string.IsNullOrEmpty(customerId)) + { + return; + } + + await stripeAdapter.PaymentMethodAttachAsync(paymentMethod.Id, + new PaymentMethodAttachOptions { Customer = customerId }); + + await stripeAdapter.CustomerUpdateAsync(customerId, new CustomerUpdateOptions + { + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + DefaultPaymentMethod = paymentMethod.Id + } + }); + + await subscriber.Match( + async organization => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(organization), + async provider => await pushNotificationAdapter.NotifyBankAccountVerifiedAsync(provider)); + } +} diff --git a/src/Billing/Services/Implementations/StripeEventProcessor.cs b/src/Billing/Services/Implementations/StripeEventProcessor.cs index b0d9cf187d..6db813f70c 100644 --- a/src/Billing/Services/Implementations/StripeEventProcessor.cs +++ b/src/Billing/Services/Implementations/StripeEventProcessor.cs @@ -3,88 +3,64 @@ using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; -public class StripeEventProcessor : IStripeEventProcessor +public class StripeEventProcessor( + ILogger logger, + ISubscriptionDeletedHandler subscriptionDeletedHandler, + ISubscriptionUpdatedHandler subscriptionUpdatedHandler, + IUpcomingInvoiceHandler upcomingInvoiceHandler, + IChargeSucceededHandler chargeSucceededHandler, + IChargeRefundedHandler chargeRefundedHandler, + IPaymentSucceededHandler paymentSucceededHandler, + IPaymentFailedHandler paymentFailedHandler, + IInvoiceCreatedHandler invoiceCreatedHandler, + IPaymentMethodAttachedHandler paymentMethodAttachedHandler, + ICustomerUpdatedHandler customerUpdatedHandler, + IInvoiceFinalizedHandler invoiceFinalizedHandler, + ISetupIntentSucceededHandler setupIntentSucceededHandler) + : IStripeEventProcessor { - private readonly ILogger _logger; - private readonly ISubscriptionDeletedHandler _subscriptionDeletedHandler; - private readonly ISubscriptionUpdatedHandler _subscriptionUpdatedHandler; - private readonly IUpcomingInvoiceHandler _upcomingInvoiceHandler; - private readonly IChargeSucceededHandler _chargeSucceededHandler; - private readonly IChargeRefundedHandler _chargeRefundedHandler; - private readonly IPaymentSucceededHandler _paymentSucceededHandler; - private readonly IPaymentFailedHandler _paymentFailedHandler; - private readonly IInvoiceCreatedHandler _invoiceCreatedHandler; - private readonly IPaymentMethodAttachedHandler _paymentMethodAttachedHandler; - private readonly ICustomerUpdatedHandler _customerUpdatedHandler; - private readonly IInvoiceFinalizedHandler _invoiceFinalizedHandler; - - public StripeEventProcessor( - ILogger logger, - ISubscriptionDeletedHandler subscriptionDeletedHandler, - ISubscriptionUpdatedHandler subscriptionUpdatedHandler, - IUpcomingInvoiceHandler upcomingInvoiceHandler, - IChargeSucceededHandler chargeSucceededHandler, - IChargeRefundedHandler chargeRefundedHandler, - IPaymentSucceededHandler paymentSucceededHandler, - IPaymentFailedHandler paymentFailedHandler, - IInvoiceCreatedHandler invoiceCreatedHandler, - IPaymentMethodAttachedHandler paymentMethodAttachedHandler, - ICustomerUpdatedHandler customerUpdatedHandler, - IInvoiceFinalizedHandler invoiceFinalizedHandler) - { - _logger = logger; - _subscriptionDeletedHandler = subscriptionDeletedHandler; - _subscriptionUpdatedHandler = subscriptionUpdatedHandler; - _upcomingInvoiceHandler = upcomingInvoiceHandler; - _chargeSucceededHandler = chargeSucceededHandler; - _chargeRefundedHandler = chargeRefundedHandler; - _paymentSucceededHandler = paymentSucceededHandler; - _paymentFailedHandler = paymentFailedHandler; - _invoiceCreatedHandler = invoiceCreatedHandler; - _paymentMethodAttachedHandler = paymentMethodAttachedHandler; - _customerUpdatedHandler = customerUpdatedHandler; - _invoiceFinalizedHandler = invoiceFinalizedHandler; - } - public async Task ProcessEventAsync(Event parsedEvent) { switch (parsedEvent.Type) { case HandledStripeWebhook.SubscriptionDeleted: - await _subscriptionDeletedHandler.HandleAsync(parsedEvent); + await subscriptionDeletedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.SubscriptionUpdated: - await _subscriptionUpdatedHandler.HandleAsync(parsedEvent); + await subscriptionUpdatedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.UpcomingInvoice: - await _upcomingInvoiceHandler.HandleAsync(parsedEvent); + await upcomingInvoiceHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.ChargeSucceeded: - await _chargeSucceededHandler.HandleAsync(parsedEvent); + await chargeSucceededHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.ChargeRefunded: - await _chargeRefundedHandler.HandleAsync(parsedEvent); + await chargeRefundedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.PaymentSucceeded: - await _paymentSucceededHandler.HandleAsync(parsedEvent); + await paymentSucceededHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.PaymentFailed: - await _paymentFailedHandler.HandleAsync(parsedEvent); + await paymentFailedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.InvoiceCreated: - await _invoiceCreatedHandler.HandleAsync(parsedEvent); + await invoiceCreatedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.PaymentMethodAttached: - await _paymentMethodAttachedHandler.HandleAsync(parsedEvent); + await paymentMethodAttachedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.CustomerUpdated: - await _customerUpdatedHandler.HandleAsync(parsedEvent); + await customerUpdatedHandler.HandleAsync(parsedEvent); break; case HandledStripeWebhook.InvoiceFinalized: - await _invoiceFinalizedHandler.HandleAsync(parsedEvent); + await invoiceFinalizedHandler.HandleAsync(parsedEvent); + break; + case HandledStripeWebhook.SetupIntentSucceeded: + await setupIntentSucceededHandler.HandleAsync(parsedEvent); break; default: - _logger.LogWarning("Unsupported event received. {EventType}", parsedEvent.Type); + logger.LogWarning("Unsupported event received. {EventType}", parsedEvent.Type); break; } } diff --git a/src/Billing/Services/Implementations/StripeEventService.cs b/src/Billing/Services/Implementations/StripeEventService.cs index 7eef357e14..03ca8eeb10 100644 --- a/src/Billing/Services/Implementations/StripeEventService.cs +++ b/src/Billing/Services/Implementations/StripeEventService.cs @@ -1,183 +1,122 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Billing.Constants; +using Bit.Billing.Constants; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; +using Bit.Core.Repositories; using Bit.Core.Settings; using Stripe; namespace Bit.Billing.Services.Implementations; -public class StripeEventService : IStripeEventService +public class StripeEventService( + GlobalSettings globalSettings, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + ISetupIntentCache setupIntentCache, + IStripeFacade stripeFacade) + : IStripeEventService { - private readonly GlobalSettings _globalSettings; - private readonly ILogger _logger; - private readonly IStripeFacade _stripeFacade; - - public StripeEventService( - GlobalSettings globalSettings, - ILogger logger, - IStripeFacade stripeFacade) + public async Task GetCharge(Event stripeEvent, bool fresh = false, List? expand = null) { - _globalSettings = globalSettings; - _logger = logger; - _stripeFacade = stripeFacade; - } - - public async Task GetCharge(Event stripeEvent, bool fresh = false, List expand = null) - { - var eventCharge = Extract(stripeEvent); + var charge = Extract(stripeEvent); if (!fresh) { - return eventCharge; + return charge; } - if (string.IsNullOrEmpty(eventCharge.Id)) - { - _logger.LogWarning("Cannot retrieve up-to-date Charge for Event with ID '{eventId}' because no Charge ID was included in the Event.", stripeEvent.Id); - return eventCharge; - } - - var charge = await _stripeFacade.GetCharge(eventCharge.Id, new ChargeGetOptions { Expand = expand }); - - if (charge == null) - { - throw new Exception( - $"Received null Charge from Stripe for ID '{eventCharge.Id}' while processing Event with ID '{stripeEvent.Id}'"); - } - - return charge; + return await stripeFacade.GetCharge(charge.Id, new ChargeGetOptions { Expand = expand }); } - public async Task GetCustomer(Event stripeEvent, bool fresh = false, List expand = null) + public async Task GetCustomer(Event stripeEvent, bool fresh = false, List? expand = null) { - var eventCustomer = Extract(stripeEvent); + var customer = Extract(stripeEvent); if (!fresh) { - return eventCustomer; + return customer; } - if (string.IsNullOrEmpty(eventCustomer.Id)) - { - _logger.LogWarning("Cannot retrieve up-to-date Customer for Event with ID '{eventId}' because no Customer ID was included in the Event.", stripeEvent.Id); - return eventCustomer; - } - - var customer = await _stripeFacade.GetCustomer(eventCustomer.Id, new CustomerGetOptions { Expand = expand }); - - if (customer == null) - { - throw new Exception( - $"Received null Customer from Stripe for ID '{eventCustomer.Id}' while processing Event with ID '{stripeEvent.Id}'"); - } - - return customer; + return await stripeFacade.GetCustomer(customer.Id, new CustomerGetOptions { Expand = expand }); } - public async Task GetInvoice(Event stripeEvent, bool fresh = false, List expand = null) + public async Task GetInvoice(Event stripeEvent, bool fresh = false, List? expand = null) { - var eventInvoice = Extract(stripeEvent); + var invoice = Extract(stripeEvent); if (!fresh) { - return eventInvoice; + return invoice; } - if (string.IsNullOrEmpty(eventInvoice.Id)) - { - _logger.LogWarning("Cannot retrieve up-to-date Invoice for Event with ID '{eventId}' because no Invoice ID was included in the Event.", stripeEvent.Id); - return eventInvoice; - } - - var invoice = await _stripeFacade.GetInvoice(eventInvoice.Id, new InvoiceGetOptions { Expand = expand }); - - if (invoice == null) - { - throw new Exception( - $"Received null Invoice from Stripe for ID '{eventInvoice.Id}' while processing Event with ID '{stripeEvent.Id}'"); - } - - return invoice; + return await stripeFacade.GetInvoice(invoice.Id, new InvoiceGetOptions { Expand = expand }); } - public async Task GetPaymentMethod(Event stripeEvent, bool fresh = false, List expand = null) + public async Task GetPaymentMethod(Event stripeEvent, bool fresh = false, + List? expand = null) { - var eventPaymentMethod = Extract(stripeEvent); + var paymentMethod = Extract(stripeEvent); if (!fresh) { - return eventPaymentMethod; + return paymentMethod; } - if (string.IsNullOrEmpty(eventPaymentMethod.Id)) - { - _logger.LogWarning("Cannot retrieve up-to-date Payment Method for Event with ID '{eventId}' because no Payment Method ID was included in the Event.", stripeEvent.Id); - return eventPaymentMethod; - } - - var paymentMethod = await _stripeFacade.GetPaymentMethod(eventPaymentMethod.Id, new PaymentMethodGetOptions { Expand = expand }); - - if (paymentMethod == null) - { - throw new Exception( - $"Received null Payment Method from Stripe for ID '{eventPaymentMethod.Id}' while processing Event with ID '{stripeEvent.Id}'"); - } - - return paymentMethod; + return await stripeFacade.GetPaymentMethod(paymentMethod.Id, new PaymentMethodGetOptions { Expand = expand }); } - public async Task GetSubscription(Event stripeEvent, bool fresh = false, List expand = null) + public async Task GetSetupIntent(Event stripeEvent, bool fresh = false, List? expand = null) { - var eventSubscription = Extract(stripeEvent); + var setupIntent = Extract(stripeEvent); if (!fresh) { - return eventSubscription; + return setupIntent; } - if (string.IsNullOrEmpty(eventSubscription.Id)) + return await stripeFacade.GetSetupIntent(setupIntent.Id, new SetupIntentGetOptions { Expand = expand }); + } + + public async Task GetSubscription(Event stripeEvent, bool fresh = false, List? expand = null) + { + var subscription = Extract(stripeEvent); + + if (!fresh) { - _logger.LogWarning("Cannot retrieve up-to-date Subscription for Event with ID '{eventId}' because no Subscription ID was included in the Event.", stripeEvent.Id); - return eventSubscription; + return subscription; } - var subscription = await _stripeFacade.GetSubscription(eventSubscription.Id, new SubscriptionGetOptions { Expand = expand }); - - if (subscription == null) - { - throw new Exception( - $"Received null Subscription from Stripe for ID '{eventSubscription.Id}' while processing Event with ID '{stripeEvent.Id}'"); - } - - return subscription; + return await stripeFacade.GetSubscription(subscription.Id, new SubscriptionGetOptions { Expand = expand }); } public async Task ValidateCloudRegion(Event stripeEvent) { - var serverRegion = _globalSettings.BaseServiceUri.CloudRegion; + var serverRegion = globalSettings.BaseServiceUri.CloudRegion; var customerExpansion = new List { "customer" }; var customerMetadata = stripeEvent.Type switch { HandledStripeWebhook.SubscriptionDeleted or HandledStripeWebhook.SubscriptionUpdated => - (await GetSubscription(stripeEvent, true, customerExpansion))?.Customer?.Metadata, + (await GetSubscription(stripeEvent, true, customerExpansion)).Customer?.Metadata, HandledStripeWebhook.ChargeSucceeded or HandledStripeWebhook.ChargeRefunded => - (await GetCharge(stripeEvent, true, customerExpansion))?.Customer?.Metadata, + (await GetCharge(stripeEvent, true, customerExpansion)).Customer?.Metadata, HandledStripeWebhook.UpcomingInvoice => await GetCustomerMetadataFromUpcomingInvoiceEvent(stripeEvent), - HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized => - (await GetInvoice(stripeEvent, true, customerExpansion))?.Customer?.Metadata, + HandledStripeWebhook.PaymentSucceeded or HandledStripeWebhook.PaymentFailed + or HandledStripeWebhook.InvoiceCreated or HandledStripeWebhook.InvoiceFinalized => + (await GetInvoice(stripeEvent, true, customerExpansion)).Customer?.Metadata, HandledStripeWebhook.PaymentMethodAttached => - (await GetPaymentMethod(stripeEvent, true, customerExpansion))?.Customer?.Metadata, + (await GetPaymentMethod(stripeEvent, true, customerExpansion)).Customer?.Metadata, HandledStripeWebhook.CustomerUpdated => - (await GetCustomer(stripeEvent, true))?.Metadata, + (await GetCustomer(stripeEvent, true)).Metadata, + + HandledStripeWebhook.SetupIntentSucceeded => + await GetCustomerMetadataFromSetupIntentSucceededEvent(stripeEvent), _ => null }; @@ -194,51 +133,69 @@ public class StripeEventService : IStripeEventService /* In Stripe, when we receive an invoice.upcoming event, the event does not include an Invoice ID because the invoice hasn't been created yet. Therefore, rather than retrieving the fresh Invoice with a 'customer' expansion, we need to use the Customer ID on the event to retrieve the metadata. */ - async Task> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent) + async Task?> GetCustomerMetadataFromUpcomingInvoiceEvent(Event localStripeEvent) { var invoice = await GetInvoice(localStripeEvent); var customer = !string.IsNullOrEmpty(invoice.CustomerId) - ? await _stripeFacade.GetCustomer(invoice.CustomerId) + ? await stripeFacade.GetCustomer(invoice.CustomerId) : null; return customer?.Metadata; } + + async Task?> GetCustomerMetadataFromSetupIntentSucceededEvent(Event localStripeEvent) + { + var setupIntent = await GetSetupIntent(localStripeEvent); + + var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id); + if (subscriberId == null) + { + return null; + } + + var organization = await organizationRepository.GetByIdAsync(subscriberId.Value); + if (organization is { GatewayCustomerId: not null }) + { + var organizationCustomer = await stripeFacade.GetCustomer(organization.GatewayCustomerId); + return organizationCustomer.Metadata; + } + + var provider = await providerRepository.GetByIdAsync(subscriberId.Value); + if (provider is not { GatewayCustomerId: not null }) + { + return null; + } + + var providerCustomer = await stripeFacade.GetCustomer(provider.GatewayCustomerId); + return providerCustomer.Metadata; + } } private static T Extract(Event stripeEvent) - { - if (stripeEvent.Data.Object is not T type) - { - throw new Exception($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'"); - } - - return type; - } + => stripeEvent.Data.Object is not T type + ? throw new Exception( + $"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{typeof(T).Name}'") + : type; private static string GetCustomerRegion(IDictionary customerMetadata) { const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates; - if (customerMetadata is null) - { - return null; - } - if (customerMetadata.TryGetValue("region", out var value)) { return value; } - var miscasedRegionKey = customerMetadata.Keys + var incorrectlyCasedRegionKey = customerMetadata.Keys .FirstOrDefault(key => key.Equals("region", StringComparison.OrdinalIgnoreCase)); - if (miscasedRegionKey is null) + if (incorrectlyCasedRegionKey is null) { return defaultRegion; } - _ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue); + _ = customerMetadata.TryGetValue(incorrectlyCasedRegionKey, out var regionValue); return !string.IsNullOrWhiteSpace(regionValue) ? regionValue diff --git a/src/Billing/Services/Implementations/StripeFacade.cs b/src/Billing/Services/Implementations/StripeFacade.cs index 726a3e977c..eef7ce009e 100644 --- a/src/Billing/Services/Implementations/StripeFacade.cs +++ b/src/Billing/Services/Implementations/StripeFacade.cs @@ -16,6 +16,7 @@ public class StripeFacade : IStripeFacade private readonly PaymentMethodService _paymentMethodService = new(); private readonly SubscriptionService _subscriptionService = new(); private readonly DiscountService _discountService = new(); + private readonly SetupIntentService _setupIntentService = new(); private readonly TestClockService _testClockService = new(); public async Task GetCharge( @@ -53,6 +54,13 @@ public class StripeFacade : IStripeFacade CancellationToken cancellationToken = default) => await _invoiceService.GetAsync(invoiceId, invoiceGetOptions, requestOptions, cancellationToken); + public async Task GetSetupIntent( + string setupIntentId, + SetupIntentGetOptions setupIntentGetOptions = null, + RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) => + await _setupIntentService.GetAsync(setupIntentId, setupIntentGetOptions, requestOptions, cancellationToken); + public async Task> ListInvoices( InvoiceListOptions options = null, RequestOptions requestOptions = null, diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index d5fcfb20d4..10630f78f4 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; -using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Quartz; @@ -25,7 +24,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IStripeFacade _stripeFacade; private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand; private readonly IUserService _userService; - private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; private readonly IOrganizationEnableCommand _organizationEnableCommand; @@ -35,6 +33,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IProviderRepository _providerRepository; private readonly IProviderService _providerService; private readonly ILogger _logger; + private readonly IPushNotificationAdapter _pushNotificationAdapter; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -43,7 +42,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IStripeFacade stripeFacade, IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand, IUserService userService, - IPushNotificationService pushNotificationService, IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, IOrganizationEnableCommand organizationEnableCommand, @@ -52,7 +50,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IFeatureService featureService, IProviderRepository providerRepository, IProviderService providerService, - ILogger logger) + ILogger logger, + IPushNotificationAdapter pushNotificationAdapter) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -61,7 +60,6 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _stripeFacade = stripeFacade; _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand; _userService = userService; - _pushNotificationService = pushNotificationService; _organizationRepository = organizationRepository; _providerRepository = providerRepository; _schedulerFactory = schedulerFactory; @@ -72,6 +70,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _providerRepository = providerRepository; _providerService = providerService; _logger = logger; + _pushNotificationAdapter = pushNotificationAdapter; } /// @@ -125,7 +124,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); if (organization != null) { - await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); + await _pushNotificationAdapter.NotifyEnabledChangedAsync(organization); } break; } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index cfbc90c36e..5b464d5ef6 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -73,6 +73,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); // Identity @@ -111,6 +112,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Add Quartz services first services.AddQuartz(q => diff --git a/src/Core/Billing/Caches/ISetupIntentCache.cs b/src/Core/Billing/Caches/ISetupIntentCache.cs index 0990266239..8e53e8fb09 100644 --- a/src/Core/Billing/Caches/ISetupIntentCache.cs +++ b/src/Core/Billing/Caches/ISetupIntentCache.cs @@ -2,9 +2,8 @@ public interface ISetupIntentCache { - Task Get(Guid subscriberId); - - Task Remove(Guid subscriberId); - + Task GetSetupIntentIdForSubscriber(Guid subscriberId); + Task GetSubscriberIdForSetupIntent(string setupIntentId); + Task RemoveSetupIntentForSubscriber(Guid subscriberId); Task Set(Guid subscriberId, string setupIntentId); } diff --git a/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs b/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs index 432a778762..8833c928fe 100644 --- a/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs +++ b/src/Core/Billing/Caches/Implementations/SetupIntentDistributedCache.cs @@ -1,7 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Billing.Caches.Implementations; @@ -10,26 +7,41 @@ public class SetupIntentDistributedCache( [FromKeyedServices("persistent")] IDistributedCache distributedCache) : ISetupIntentCache { - public async Task Get(Guid subscriberId) + public async Task GetSetupIntentIdForSubscriber(Guid subscriberId) { - var cacheKey = GetCacheKey(subscriberId); - + var cacheKey = GetCacheKeyBySubscriberId(subscriberId); return await distributedCache.GetStringAsync(cacheKey); } - public async Task Remove(Guid subscriberId) + public async Task GetSubscriberIdForSetupIntent(string setupIntentId) { - var cacheKey = GetCacheKey(subscriberId); + var cacheKey = GetCacheKeyBySetupIntentId(setupIntentId); + var value = await distributedCache.GetStringAsync(cacheKey); + if (string.IsNullOrEmpty(value) || !Guid.TryParse(value, out var subscriberId)) + { + return null; + } + return subscriberId; + } + public async Task RemoveSetupIntentForSubscriber(Guid subscriberId) + { + var cacheKey = GetCacheKeyBySubscriberId(subscriberId); await distributedCache.RemoveAsync(cacheKey); } public async Task Set(Guid subscriberId, string setupIntentId) { - var cacheKey = GetCacheKey(subscriberId); - - await distributedCache.SetStringAsync(cacheKey, setupIntentId); + var bySubscriberIdCacheKey = GetCacheKeyBySubscriberId(subscriberId); + var bySetupIntentIdCacheKey = GetCacheKeyBySetupIntentId(setupIntentId); + await Task.WhenAll( + distributedCache.SetStringAsync(bySubscriberIdCacheKey, setupIntentId), + distributedCache.SetStringAsync(bySetupIntentIdCacheKey, subscriberId.ToString())); } - private static string GetCacheKey(Guid subscriberId) => $"pending_bank_account_{subscriberId}"; + private static string GetCacheKeyBySetupIntentId(string setupIntentId) => + $"subscriber_id_for_setup_intent_id_{setupIntentId}"; + + private static string GetCacheKeyBySubscriberId(Guid subscriberId) => + $"setup_intent_id_for_subscriber_id_{subscriberId}"; } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index 0b0cbd22c6..312623ffa5 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -285,7 +285,7 @@ public class GetOrganizationWarningsQuery( private async Task HasUnverifiedBankAccountAsync( Organization organization) { - var setupIntentId = await setupIntentCache.Get(organization.Id); + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id); if (string.IsNullOrEmpty(setupIntentId)) { diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 446f9563f9..ce8a9a877b 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -383,7 +383,7 @@ public class OrganizationBillingService( { case PaymentMethodType.BankAccount: { - await setupIntentCache.Remove(organization.Id); + await setupIntentCache.RemoveSetupIntentForSubscriber(organization.Id); break; } case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): diff --git a/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs b/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs deleted file mode 100644 index 4f3e38707c..0000000000 --- a/src/Core/Billing/Payment/Commands/VerifyBankAccountCommand.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Bit.Core.Billing.Caches; -using Bit.Core.Billing.Commands; -using Bit.Core.Billing.Payment.Models; -using Bit.Core.Entities; -using Bit.Core.Services; -using Microsoft.Extensions.Logging; -using Stripe; - -namespace Bit.Core.Billing.Payment.Commands; - -public interface IVerifyBankAccountCommand -{ - Task> Run( - ISubscriber subscriber, - string descriptorCode); -} - -public class VerifyBankAccountCommand( - ILogger logger, - ISetupIntentCache setupIntentCache, - IStripeAdapter stripeAdapter) : BaseBillingCommand(logger), IVerifyBankAccountCommand -{ - private readonly ILogger _logger = logger; - - protected override Conflict DefaultConflict - => new("We had a problem verifying your bank account. Please contact support for assistance."); - - public Task> Run( - ISubscriber subscriber, - string descriptorCode) => HandleAsync(async () => - { - var setupIntentId = await setupIntentCache.Get(subscriber.Id); - - if (string.IsNullOrEmpty(setupIntentId)) - { - _logger.LogError( - "{Command}: Could not find setup intent to verify subscriber's ({SubscriberID}) bank account", - CommandName, subscriber.Id); - return DefaultConflict; - } - - await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, - new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode }); - - var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, - new SetupIntentGetOptions { Expand = ["payment_method"] }); - - var paymentMethod = await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, - new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - - await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, - new CustomerUpdateOptions - { - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = setupIntent.PaymentMethodId - } - }); - - return MaskedPaymentMethod.From(paymentMethod.UsBankAccount); - }); -} diff --git a/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs index d23ca75025..d30c27ee41 100644 --- a/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs +++ b/src/Core/Billing/Payment/Models/MaskedPaymentMethod.cs @@ -10,7 +10,7 @@ public record MaskedBankAccount { public required string BankName { get; init; } public required string Last4 { get; init; } - public required bool Verified { get; init; } + public string? HostedVerificationUrl { get; init; } public string Type => "bankAccount"; } @@ -39,8 +39,7 @@ public class MaskedPaymentMethod(OneOf new MaskedBankAccount { BankName = bankAccount.BankName, - Last4 = bankAccount.Last4, - Verified = bankAccount.Status == "verified" + Last4 = bankAccount.Last4 }; public static MaskedPaymentMethod From(Card card) => new MaskedCard @@ -61,7 +60,7 @@ public class MaskedPaymentMethod(OneOf new MaskedCard @@ -74,8 +73,7 @@ public class MaskedPaymentMethod(OneOf new MaskedBankAccount { BankName = bankAccount.BankName, - Last4 = bankAccount.Last4, - Verified = true + Last4 = bankAccount.Last4 }; public static MaskedPaymentMethod From(PayPalAccount payPalAccount) => new MaskedPayPalAccount { Email = payPalAccount.Email }; diff --git a/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs index ce8f031a5d..9f9618571e 100644 --- a/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs +++ b/src/Core/Billing/Payment/Queries/GetPaymentMethodQuery.cs @@ -33,6 +33,7 @@ public class GetPaymentMethodQuery( return null; } + // First check for PayPal if (customer.Metadata.TryGetValue(StripeConstants.MetadataKeys.BraintreeCustomerId, out var braintreeCustomerId)) { var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId); @@ -47,6 +48,23 @@ public class GetPaymentMethodQuery( return null; } + // Then check for a bank account pending verification + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id); + + if (!string.IsNullOrEmpty(setupIntentId)) + { + var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }); + + if (setupIntent.IsUnverifiedBankAccount()) + { + return MaskedPaymentMethod.From(setupIntent); + } + } + + // Then check the default payment method var paymentMethod = customer.InvoiceSettings.DefaultPaymentMethod != null ? customer.InvoiceSettings.DefaultPaymentMethod.Type switch { @@ -61,40 +79,12 @@ public class GetPaymentMethodQuery( return paymentMethod; } - if (customer.DefaultSource != null) + return customer.DefaultSource switch { - paymentMethod = customer.DefaultSource switch - { - Card card => MaskedPaymentMethod.From(card), - BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount), - Source { Card: not null } source => MaskedPaymentMethod.From(source.Card), - _ => null - }; - - if (paymentMethod != null) - { - return paymentMethod; - } - } - - var setupIntentId = await setupIntentCache.Get(subscriber.Id); - - if (string.IsNullOrEmpty(setupIntentId)) - { - return null; - } - - var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions - { - Expand = ["payment_method"] - }); - - // ReSharper disable once ConvertIfStatementToReturnStatement - if (!setupIntent.IsUnverifiedBankAccount()) - { - return null; - } - - return MaskedPaymentMethod.From(setupIntent); + Card card => MaskedPaymentMethod.From(card), + BankAccount bankAccount => MaskedPaymentMethod.From(bankAccount), + Source { Card: not null } source => MaskedPaymentMethod.From(source.Card), + _ => null + }; } } diff --git a/src/Core/Billing/Payment/Registrations.cs b/src/Core/Billing/Payment/Registrations.cs index 1cc7914f10..478673d2fc 100644 --- a/src/Core/Billing/Payment/Registrations.cs +++ b/src/Core/Billing/Payment/Registrations.cs @@ -14,7 +14,6 @@ public static class Registrations services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); // Queries services.AddTransient(); diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 5b1b717c20..986991ba0a 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -283,7 +283,7 @@ public class PremiumUserBillingService( { case PaymentMethodType.BankAccount: { - await setupIntentCache.Remove(user.Id); + await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id); break; } case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 378e84f15a..1206397d9e 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -858,7 +858,7 @@ public class SubscriberService( ISubscriber subscriber, string descriptorCode) { - var setupIntentId = await setupIntentCache.Get(subscriber.Id); + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id); if (string.IsNullOrEmpty(setupIntentId)) { @@ -986,7 +986,7 @@ public class SubscriberService( * attachedPaymentMethodDTO being null represents a case where we could be looking for the SetupIntent for an unverified "us_bank_account". * We store the ID of this SetupIntent in the cache when we originally update the payment method. */ - var setupIntentId = await setupIntentCache.Get(subscriberId); + var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriberId); if (string.IsNullOrEmpty(setupIntentId)) { diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index e235d05b13..c4ae1e2858 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -86,3 +86,14 @@ public class OrganizationCollectionManagementPushNotification public bool LimitCollectionDeletion { get; init; } public bool LimitItemDeletion { get; init; } } + +public class OrganizationBankAccountVerifiedPushNotification +{ + public Guid OrganizationId { get; set; } +} + +public class ProviderBankAccountVerifiedPushNotification +{ + public Guid ProviderId { get; set; } + public Guid AdminId { get; set; } +} diff --git a/src/Core/Platform/Push/IPushNotificationService.cs b/src/Core/Platform/Push/IPushNotificationService.cs index 339ce5a917..32a488b827 100644 --- a/src/Core/Platform/Push/IPushNotificationService.cs +++ b/src/Core/Platform/Push/IPushNotificationService.cs @@ -399,20 +399,6 @@ public interface IPushNotificationService ExcludeCurrentContext = true, }); - Task PushSyncOrganizationStatusAsync(Organization organization) - => PushAsync(new PushNotification - { - Type = PushType.SyncOrganizationStatusChanged, - Target = NotificationTarget.Organization, - TargetId = organization.Id, - Payload = new OrganizationStatusPushNotification - { - OrganizationId = organization.Id, - Enabled = organization.Enabled, - }, - ExcludeCurrentContext = false, - }); - Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => PushAsync(new PushNotification { diff --git a/src/Core/Platform/Push/PushType.cs b/src/Core/Platform/Push/PushType.cs index 7fcb60b4ef..7765c1aa66 100644 --- a/src/Core/Platform/Push/PushType.cs +++ b/src/Core/Platform/Push/PushType.cs @@ -4,16 +4,16 @@ namespace Bit.Core.Enums; /// -/// +/// /// /// /// -/// When adding a new enum member you must annotate it with a +/// When adding a new enum member you must annotate it with a /// this is enforced with a unit test. It is preferred that you do NOT add new usings for the type referenced /// in . /// /// -/// You may and are +/// You may and are /// /// public enum PushType : byte @@ -90,4 +90,10 @@ public enum PushType : byte [NotificationInfo("@bitwarden/team-vault-dev", typeof(Models.UserPushNotification))] RefreshSecurityTasks = 22, + + [NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))] + OrganizationBankAccountVerified = 23, + + [NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))] + ProviderBankAccountVerified = 24 } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index f49ca96ea4..69d5bdc958 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -106,6 +106,20 @@ public static class HubHelpers await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId)) .SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken); break; + case PushType.OrganizationBankAccountVerified: + var organizationBankAccountVerifiedNotification = + JsonSerializer.Deserialize>( + notificationJson, _deserializerOptions); + 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()) + .SendAsync(_receiveMessageMethod, providerBankAccountVerifiedNotification, cancellationToken); + break; case PushType.Notification: case PushType.NotificationStatus: var notificationData = JsonSerializer.Deserialize>( @@ -144,6 +158,7 @@ public static 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); break; } } diff --git a/test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs b/test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs new file mode 100644 index 0000000000..e9f0d9d0ed --- /dev/null +++ b/test/Billing.Test/Services/SetupIntentSucceededHandlerTests.cs @@ -0,0 +1,242 @@ +using Bit.Billing.Services; +using Bit.Billing.Services.Implementations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; +using Bit.Core.Repositories; +using Bit.Core.Services; +using NSubstitute; +using Stripe; +using Xunit; +using Event = Stripe.Event; + +namespace Bit.Billing.Test.Services; + +public class SetupIntentSucceededHandlerTests +{ + private static readonly Event _mockEvent = new() { Id = "evt_test", Type = "setup_intent.succeeded" }; + private static readonly string[] _expand = ["payment_method"]; + + private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderRepository _providerRepository; + private readonly IPushNotificationAdapter _pushNotificationAdapter; + private readonly ISetupIntentCache _setupIntentCache; + private readonly IStripeAdapter _stripeAdapter; + private readonly IStripeEventService _stripeEventService; + private readonly SetupIntentSucceededHandler _handler; + + public SetupIntentSucceededHandlerTests() + { + _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); + _pushNotificationAdapter = Substitute.For(); + _setupIntentCache = Substitute.For(); + _stripeAdapter = Substitute.For(); + _stripeEventService = Substitute.For(); + + _handler = new SetupIntentSucceededHandler( + _organizationRepository, + _providerRepository, + _pushNotificationAdapter, + _setupIntentCache, + _stripeAdapter, + _stripeEventService); + } + + [Fact] + public async Task HandleAsync_PaymentMethodNotUSBankAccount_Returns() + { + // Arrange + var setupIntent = CreateSetupIntent(hasUSBankAccount: false); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _setupIntentCache.DidNotReceiveWithAnyArgs().GetSubscriberIdForSetupIntent(Arg.Any()); + await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync( + Arg.Any(), Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_NoSubscriberIdInCache_Returns() + { + // Arrange + var setupIntent = CreateSetupIntent(); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id) + .Returns((Guid?)null); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync( + Arg.Any(), Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ValidOrganization_AttachesPaymentMethodAndSendsNotification() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = "cus_test" }; + var setupIntent = CreateSetupIntent(); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id) + .Returns(organizationId); + + _organizationRepository.GetByIdAsync(organizationId) + .Returns(organization); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _stripeAdapter.Received(1).PaymentMethodAttachAsync( + "pm_test", + Arg.Is(o => o.Customer == organization.GatewayCustomerId)); + + await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(organization); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ValidProvider_AttachesPaymentMethodAndSendsNotification() + { + // Arrange + var providerId = Guid.NewGuid(); + var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = "cus_test" }; + var setupIntent = CreateSetupIntent(); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id) + .Returns(providerId); + + _organizationRepository.GetByIdAsync(providerId) + .Returns((Organization?)null); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _stripeAdapter.Received(1).PaymentMethodAttachAsync( + "pm_test", + Arg.Is(o => o.Customer == provider.GatewayCustomerId)); + + await _pushNotificationAdapter.Received(1).NotifyBankAccountVerifiedAsync(provider); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_OrganizationWithoutGatewayCustomerId_DoesNotAttachPaymentMethod() + { + // Arrange + var organizationId = Guid.NewGuid(); + var organization = new Organization { Id = organizationId, Name = "Test Org", GatewayCustomerId = null }; + var setupIntent = CreateSetupIntent(); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id) + .Returns(organizationId); + + _organizationRepository.GetByIdAsync(organizationId) + .Returns(organization); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync( + Arg.Any(), Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + [Fact] + public async Task HandleAsync_ProviderWithoutGatewayCustomerId_DoesNotAttachPaymentMethod() + { + // Arrange + var providerId = Guid.NewGuid(); + var provider = new Provider { Id = providerId, Name = "Test Provider", GatewayCustomerId = null }; + var setupIntent = CreateSetupIntent(); + + _stripeEventService.GetSetupIntent( + _mockEvent, + true, + Arg.Is>(options => options.SequenceEqual(_expand))) + .Returns(setupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id) + .Returns(providerId); + + _organizationRepository.GetByIdAsync(providerId) + .Returns((Organization?)null); + + _providerRepository.GetByIdAsync(providerId) + .Returns(provider); + + // Act + await _handler.HandleAsync(_mockEvent); + + // Assert + await _stripeAdapter.DidNotReceiveWithAnyArgs().PaymentMethodAttachAsync( + Arg.Any(), Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + await _pushNotificationAdapter.DidNotReceiveWithAnyArgs().NotifyBankAccountVerifiedAsync(Arg.Any()); + } + + private static SetupIntent CreateSetupIntent(bool hasUSBankAccount = true) + { + var paymentMethod = new PaymentMethod + { + Id = "pm_test", + Type = "us_bank_account", + UsBankAccount = hasUSBankAccount ? new PaymentMethodUsBankAccount() : null + }; + + var setupIntent = new SetupIntent + { + Id = "seti_test", + PaymentMethod = paymentMethod + }; + + return setupIntent; + } +} diff --git a/test/Billing.Test/Services/StripeEventServiceTests.cs b/test/Billing.Test/Services/StripeEventServiceTests.cs index b40e8b9408..68aeab2f44 100644 --- a/test/Billing.Test/Services/StripeEventServiceTests.cs +++ b/test/Billing.Test/Services/StripeEventServiceTests.cs @@ -1,8 +1,9 @@ using Bit.Billing.Services; using Bit.Billing.Services.Implementations; -using Bit.Billing.Test.Utilities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; +using Bit.Core.Repositories; using Bit.Core.Settings; -using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; @@ -11,6 +12,9 @@ namespace Bit.Billing.Test.Services; public class StripeEventServiceTests { + private readonly IOrganizationRepository _organizationRepository; + private readonly IProviderRepository _providerRepository; + private readonly ISetupIntentCache _setupIntentCache; private readonly IStripeFacade _stripeFacade; private readonly StripeEventService _stripeEventService; @@ -20,8 +24,11 @@ public class StripeEventServiceTests var baseServiceUriSettings = new GlobalSettings.BaseServiceUriSettings(globalSettings) { CloudRegion = "US" }; globalSettings.BaseServiceUri = baseServiceUriSettings; + _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); + _setupIntentCache = Substitute.For(); _stripeFacade = Substitute.For(); - _stripeEventService = new StripeEventService(globalSettings, Substitute.For>(), _stripeFacade); + _stripeEventService = new StripeEventService(globalSettings, _organizationRepository, _providerRepository, _setupIntentCache, _stripeFacade); } #region GetCharge @@ -29,50 +36,44 @@ public class StripeEventServiceTests public async Task GetCharge_EventNotChargeRelated_ThrowsException() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" }); - // Act - var function = async () => await _stripeEventService.GetCharge(stripeEvent); - - // Assert - var exception = await Assert.ThrowsAsync(function); + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetCharge(stripeEvent)); Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Charge)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetCharge_NotFresh_ReturnsEventCharge() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded); + var mockCharge = new Charge { Id = "ch_test", Amount = 1000 }; + var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge); // Act var charge = await _stripeEventService.GetCharge(stripeEvent); // Assert - Assert.Equivalent(stripeEvent.Data.Object as Charge, charge, true); + Assert.Equal(mockCharge.Id, charge.Id); + Assert.Equal(mockCharge.Amount, charge.Amount); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCharge( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetCharge_Fresh_Expand_ReturnsAPICharge() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded); + var eventCharge = new Charge { Id = "ch_test", Amount = 1000 }; + var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", eventCharge); - var eventCharge = stripeEvent.Data.Object as Charge; - - var apiCharge = Copy(eventCharge); + var apiCharge = new Charge { Id = "ch_test", Amount = 2000 }; var expand = new List { "customer" }; @@ -90,9 +91,7 @@ public class StripeEventServiceTests await _stripeFacade.Received().GetCharge( apiCharge.Id, - Arg.Is(options => options.Expand == expand), - Arg.Any(), - Arg.Any()); + Arg.Is(options => options.Expand == expand)); } #endregion @@ -101,50 +100,44 @@ public class StripeEventServiceTests public async Task GetCustomer_EventNotCustomerRelated_ThrowsException() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var stripeEvent = CreateMockEvent("evt_test", "invoice.created", new Invoice { Id = "in_test" }); - // Act - var function = async () => await _stripeEventService.GetCustomer(stripeEvent); - - // Assert - var exception = await Assert.ThrowsAsync(function); + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetCustomer(stripeEvent)); Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Customer)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetCustomer_NotFresh_ReturnsEventCustomer() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); + var mockCustomer = new Customer { Id = "cus_test", Email = "test@example.com" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer); // Act var customer = await _stripeEventService.GetCustomer(stripeEvent); // Assert - Assert.Equivalent(stripeEvent.Data.Object as Customer, customer, true); + Assert.Equal(mockCustomer.Id, customer.Id); + Assert.Equal(mockCustomer.Email, customer.Email); await _stripeFacade.DidNotReceiveWithAnyArgs().GetCustomer( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetCustomer_Fresh_Expand_ReturnsAPICustomer() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); + var eventCustomer = new Customer { Id = "cus_test", Email = "test@example.com" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", eventCustomer); - var eventCustomer = stripeEvent.Data.Object as Customer; - - var apiCustomer = Copy(eventCustomer); + var apiCustomer = new Customer { Id = "cus_test", Email = "updated@example.com" }; var expand = new List { "subscriptions" }; @@ -162,9 +155,7 @@ public class StripeEventServiceTests await _stripeFacade.Received().GetCustomer( apiCustomer.Id, - Arg.Is(options => options.Expand == expand), - Arg.Any(), - Arg.Any()); + Arg.Is(options => options.Expand == expand)); } #endregion @@ -173,50 +164,44 @@ public class StripeEventServiceTests public async Task GetInvoice_EventNotInvoiceRelated_ThrowsException() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" }); - // Act - var function = async () => await _stripeEventService.GetInvoice(stripeEvent); - - // Assert - var exception = await Assert.ThrowsAsync(function); + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetInvoice(stripeEvent)); Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Invoice)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetInvoice_NotFresh_ReturnsEventInvoice() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var mockInvoice = new Invoice { Id = "in_test", AmountDue = 1000 }; + var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice); // Act var invoice = await _stripeEventService.GetInvoice(stripeEvent); // Assert - Assert.Equivalent(stripeEvent.Data.Object as Invoice, invoice, true); + Assert.Equal(mockInvoice.Id, invoice.Id); + Assert.Equal(mockInvoice.AmountDue, invoice.AmountDue); await _stripeFacade.DidNotReceiveWithAnyArgs().GetInvoice( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetInvoice_Fresh_Expand_ReturnsAPIInvoice() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var eventInvoice = new Invoice { Id = "in_test", AmountDue = 1000 }; + var stripeEvent = CreateMockEvent("evt_test", "invoice.created", eventInvoice); - var eventInvoice = stripeEvent.Data.Object as Invoice; - - var apiInvoice = Copy(eventInvoice); + var apiInvoice = new Invoice { Id = "in_test", AmountDue = 2000 }; var expand = new List { "customer" }; @@ -234,9 +219,7 @@ public class StripeEventServiceTests await _stripeFacade.Received().GetInvoice( apiInvoice.Id, - Arg.Is(options => options.Expand == expand), - Arg.Any(), - Arg.Any()); + Arg.Is(options => options.Expand == expand)); } #endregion @@ -245,50 +228,44 @@ public class StripeEventServiceTests public async Task GetPaymentMethod_EventNotPaymentMethodRelated_ThrowsException() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" }); - // Act - var function = async () => await _stripeEventService.GetPaymentMethod(stripeEvent); - - // Assert - var exception = await Assert.ThrowsAsync(function); + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetPaymentMethod(stripeEvent)); Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(PaymentMethod)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetPaymentMethod_NotFresh_ReturnsEventPaymentMethod() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached); + var mockPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" }; + var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod); // Act var paymentMethod = await _stripeEventService.GetPaymentMethod(stripeEvent); // Assert - Assert.Equivalent(stripeEvent.Data.Object as PaymentMethod, paymentMethod, true); + Assert.Equal(mockPaymentMethod.Id, paymentMethod.Id); + Assert.Equal(mockPaymentMethod.Type, paymentMethod.Type); await _stripeFacade.DidNotReceiveWithAnyArgs().GetPaymentMethod( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetPaymentMethod_Fresh_Expand_ReturnsAPIPaymentMethod() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached); + var eventPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" }; + var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", eventPaymentMethod); - var eventPaymentMethod = stripeEvent.Data.Object as PaymentMethod; - - var apiPaymentMethod = Copy(eventPaymentMethod); + var apiPaymentMethod = new PaymentMethod { Id = "pm_test", Type = "card" }; var expand = new List { "customer" }; @@ -306,9 +283,7 @@ public class StripeEventServiceTests await _stripeFacade.Received().GetPaymentMethod( apiPaymentMethod.Id, - Arg.Is(options => options.Expand == expand), - Arg.Any(), - Arg.Any()); + Arg.Is(options => options.Expand == expand)); } #endregion @@ -317,50 +292,44 @@ public class StripeEventServiceTests public async Task GetSubscription_EventNotSubscriptionRelated_ThrowsException() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" }); - // Act - var function = async () => await _stripeEventService.GetSubscription(stripeEvent); - - // Assert - var exception = await Assert.ThrowsAsync(function); + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetSubscription(stripeEvent)); Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(Subscription)}'", exception.Message); await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetSubscription_NotFresh_ReturnsEventSubscription() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var mockSubscription = new Subscription { Id = "sub_test", Status = "active" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription); // Act var subscription = await _stripeEventService.GetSubscription(stripeEvent); // Assert - Assert.Equivalent(stripeEvent.Data.Object as Subscription, subscription, true); + Assert.Equal(mockSubscription.Id, subscription.Id); + Assert.Equal(mockSubscription.Status, subscription.Status); await _stripeFacade.DidNotReceiveWithAnyArgs().GetSubscription( Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any()); + Arg.Any()); } [Fact] public async Task GetSubscription_Fresh_Expand_ReturnsAPISubscription() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var eventSubscription = new Subscription { Id = "sub_test", Status = "active" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", eventSubscription); - var eventSubscription = stripeEvent.Data.Object as Subscription; - - var apiSubscription = Copy(eventSubscription); + var apiSubscription = new Subscription { Id = "sub_test", Status = "canceled" }; var expand = new List { "customer" }; @@ -378,9 +347,71 @@ public class StripeEventServiceTests await _stripeFacade.Received().GetSubscription( apiSubscription.Id, - Arg.Is(options => options.Expand == expand), - Arg.Any(), - Arg.Any()); + Arg.Is(options => options.Expand == expand)); + } + #endregion + + #region GetSetupIntent + [Fact] + public async Task GetSetupIntent_EventNotSetupIntentRelated_ThrowsException() + { + // Arrange + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", new Customer { Id = "cus_test" }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => await _stripeEventService.GetSetupIntent(stripeEvent)); + Assert.Equal($"Stripe event with ID '{stripeEvent.Id}' does not have object matching type '{nameof(SetupIntent)}'", exception.Message); + + await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent( + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task GetSetupIntent_NotFresh_ReturnsEventSetupIntent() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + + // Act + var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent); + + // Assert + Assert.Equal(mockSetupIntent.Id, setupIntent.Id); + Assert.Equal(mockSetupIntent.Status, setupIntent.Status); + + await _stripeFacade.DidNotReceiveWithAnyArgs().GetSetupIntent( + Arg.Any(), + Arg.Any()); + } + + [Fact] + public async Task GetSetupIntent_Fresh_Expand_ReturnsAPISetupIntent() + { + // Arrange + var eventSetupIntent = new SetupIntent { Id = "seti_test", Status = "succeeded" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", eventSetupIntent); + + var apiSetupIntent = new SetupIntent { Id = "seti_test", Status = "requires_action" }; + + var expand = new List { "customer" }; + + _stripeFacade.GetSetupIntent( + apiSetupIntent.Id, + Arg.Is(options => options.Expand == expand)) + .Returns(apiSetupIntent); + + // Act + var setupIntent = await _stripeEventService.GetSetupIntent(stripeEvent, true, expand); + + // Assert + Assert.Equal(apiSetupIntent, setupIntent); + Assert.NotSame(eventSetupIntent, setupIntent); + + await _stripeFacade.Received().GetSetupIntent( + apiSetupIntent.Id, + Arg.Is(options => options.Expand == expand)); } #endregion @@ -389,18 +420,16 @@ public class StripeEventServiceTests public async Task ValidateCloudRegion_SubscriptionUpdated_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var mockSubscription = new Subscription { Id = "sub_test" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription); - var subscription = Copy(stripeEvent.Data.Object as Subscription); - - var customer = await GetCustomerAsync(); - - subscription.Customer = customer; + var customer = CreateMockCustomer(); + mockSubscription.Customer = customer; _stripeFacade.GetSubscription( - subscription.Id, + mockSubscription.Id, Arg.Any()) - .Returns(subscription); + .Returns(mockSubscription); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -409,28 +438,24 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( - subscription.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockSubscription.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_ChargeSucceeded_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.ChargeSucceeded); + var mockCharge = new Charge { Id = "ch_test" }; + var stripeEvent = CreateMockEvent("evt_test", "charge.succeeded", mockCharge); - var charge = Copy(stripeEvent.Data.Object as Charge); - - var customer = await GetCustomerAsync(); - - charge.Customer = customer; + var customer = CreateMockCustomer(); + mockCharge.Customer = customer; _stripeFacade.GetCharge( - charge.Id, + mockCharge.Id, Arg.Any()) - .Returns(charge); + .Returns(mockCharge); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -439,24 +464,21 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCharge( - charge.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockCharge.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_UpcomingInvoice_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceUpcoming); + var mockInvoice = new Invoice { Id = "in_test", CustomerId = "cus_test" }; + var stripeEvent = CreateMockEvent("evt_test", "invoice.upcoming", mockInvoice); - var invoice = Copy(stripeEvent.Data.Object as Invoice); - - var customer = await GetCustomerAsync(); + var customer = CreateMockCustomer(); _stripeFacade.GetCustomer( - invoice.CustomerId, + mockInvoice.CustomerId, Arg.Any()) .Returns(customer); @@ -467,28 +489,24 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCustomer( - invoice.CustomerId, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockInvoice.CustomerId, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_InvoiceCreated_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.InvoiceCreated); + var mockInvoice = new Invoice { Id = "in_test" }; + var stripeEvent = CreateMockEvent("evt_test", "invoice.created", mockInvoice); - var invoice = Copy(stripeEvent.Data.Object as Invoice); - - var customer = await GetCustomerAsync(); - - invoice.Customer = customer; + var customer = CreateMockCustomer(); + mockInvoice.Customer = customer; _stripeFacade.GetInvoice( - invoice.Id, + mockInvoice.Id, Arg.Any()) - .Returns(invoice); + .Returns(mockInvoice); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -497,28 +515,24 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetInvoice( - invoice.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockInvoice.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_PaymentMethodAttached_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.PaymentMethodAttached); + var mockPaymentMethod = new PaymentMethod { Id = "pm_test" }; + var stripeEvent = CreateMockEvent("evt_test", "payment_method.attached", mockPaymentMethod); - var paymentMethod = Copy(stripeEvent.Data.Object as PaymentMethod); - - var customer = await GetCustomerAsync(); - - paymentMethod.Customer = customer; + var customer = CreateMockCustomer(); + mockPaymentMethod.Customer = customer; _stripeFacade.GetPaymentMethod( - paymentMethod.Id, + mockPaymentMethod.Id, Arg.Any()) - .Returns(paymentMethod); + .Returns(mockPaymentMethod); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -527,24 +541,21 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetPaymentMethod( - paymentMethod.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockPaymentMethod.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_CustomerUpdated_Success() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated); - - var customer = Copy(stripeEvent.Data.Object as Customer); + var mockCustomer = CreateMockCustomer(); + var stripeEvent = CreateMockEvent("evt_test", "customer.updated", mockCustomer); _stripeFacade.GetCustomer( - customer.Id, + mockCustomer.Id, Arg.Any()) - .Returns(customer); + .Returns(mockCustomer); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -553,29 +564,24 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetCustomer( - customer.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockCustomer.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_MetadataNull_ReturnsFalse() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var mockSubscription = new Subscription { Id = "sub_test" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription); - var subscription = Copy(stripeEvent.Data.Object as Subscription); - - var customer = await GetCustomerAsync(); - customer.Metadata = null; - - subscription.Customer = customer; + var customer = new Customer { Id = "cus_test", Metadata = null }; + mockSubscription.Customer = customer; _stripeFacade.GetSubscription( - subscription.Id, + mockSubscription.Id, Arg.Any()) - .Returns(subscription); + .Returns(mockSubscription); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -584,29 +590,24 @@ public class StripeEventServiceTests Assert.False(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( - subscription.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockSubscription.Id, + Arg.Any()); } [Fact] public async Task ValidateCloudRegion_MetadataNoRegion_DefaultUS_ReturnsTrue() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var mockSubscription = new Subscription { Id = "sub_test" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription); - var subscription = Copy(stripeEvent.Data.Object as Subscription); - - var customer = await GetCustomerAsync(); - customer.Metadata = new Dictionary(); - - subscription.Customer = customer; + var customer = new Customer { Id = "cus_test", Metadata = new Dictionary() }; + mockSubscription.Customer = customer; _stripeFacade.GetSubscription( - subscription.Id, + mockSubscription.Id, Arg.Any()) - .Returns(subscription); + .Returns(mockSubscription); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -615,32 +616,28 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( - subscription.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockSubscription.Id, + Arg.Any()); } [Fact] - public async Task ValidateCloudRegion_MetadataMiscasedRegion_ReturnsTrue() + public async Task ValidateCloudRegion_MetadataIncorrectlyCasedRegion_ReturnsTrue() { // Arrange - var stripeEvent = await StripeTestEvents.GetAsync(StripeEventType.CustomerSubscriptionUpdated); + var mockSubscription = new Subscription { Id = "sub_test" }; + var stripeEvent = CreateMockEvent("evt_test", "customer.subscription.updated", mockSubscription); - var subscription = Copy(stripeEvent.Data.Object as Subscription); - - var customer = await GetCustomerAsync(); - customer.Metadata = new Dictionary + var customer = new Customer { - { "Region", "US" } + Id = "cus_test", + Metadata = new Dictionary { { "Region", "US" } } }; - - subscription.Customer = customer; + mockSubscription.Customer = customer; _stripeFacade.GetSubscription( - subscription.Id, + mockSubscription.Id, Arg.Any()) - .Returns(subscription); + .Returns(mockSubscription); // Act var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); @@ -649,31 +646,209 @@ public class StripeEventServiceTests Assert.True(cloudRegionValid); await _stripeFacade.Received(1).GetSubscription( - subscription.Id, - Arg.Any(), - Arg.Any(), - Arg.Any()); + mockSubscription.Id, + Arg.Any()); + } + + [Fact] + public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationCustomer_Success() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + var organizationId = Guid.NewGuid(); + var organizationCustomerId = "cus_org_test"; + + var mockOrganization = new Core.AdminConsole.Entities.Organization + { + Id = organizationId, + GatewayCustomerId = organizationCustomerId + }; + var customer = CreateMockCustomer(); + + _setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id) + .Returns(organizationId); + + _organizationRepository.GetByIdAsync(organizationId) + .Returns(mockOrganization); + + _stripeFacade.GetCustomer(organizationCustomerId) + .Returns(customer); + + // Act + var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); + + // Assert + Assert.True(cloudRegionValid); + + await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id); + await _organizationRepository.Received(1).GetByIdAsync(organizationId); + await _stripeFacade.Received(1).GetCustomer(organizationCustomerId); + } + + [Fact] + public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderCustomer_Success() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + var providerId = Guid.NewGuid(); + var providerCustomerId = "cus_provider_test"; + + var mockProvider = new Core.AdminConsole.Entities.Provider.Provider + { + Id = providerId, + GatewayCustomerId = providerCustomerId + }; + var customer = CreateMockCustomer(); + + _setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id) + .Returns(providerId); + + _organizationRepository.GetByIdAsync(providerId) + .Returns((Core.AdminConsole.Entities.Organization?)null); + + _providerRepository.GetByIdAsync(providerId) + .Returns(mockProvider); + + _stripeFacade.GetCustomer(providerCustomerId) + .Returns(customer); + + // Act + var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); + + // Assert + Assert.True(cloudRegionValid); + + await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id); + await _organizationRepository.Received(1).GetByIdAsync(providerId); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _stripeFacade.Received(1).GetCustomer(providerCustomerId); + } + + [Fact] + public async Task ValidateCloudRegion_SetupIntentSucceeded_NoSubscriberIdInCache_ReturnsFalse() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + + _setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id) + .Returns((Guid?)null); + + // Act + var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); + + // Assert + Assert.False(cloudRegionValid); + + await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id); + await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await _providerRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any()); + } + + [Fact] + public async Task ValidateCloudRegion_SetupIntentSucceeded_OrganizationWithoutGatewayCustomerId_ChecksProvider() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + var subscriberId = Guid.NewGuid(); + var providerCustomerId = "cus_provider_test"; + + var mockOrganizationWithoutCustomerId = new Core.AdminConsole.Entities.Organization + { + Id = subscriberId, + GatewayCustomerId = null + }; + + var mockProvider = new Core.AdminConsole.Entities.Provider.Provider + { + Id = subscriberId, + GatewayCustomerId = providerCustomerId + }; + var customer = CreateMockCustomer(); + + _setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id) + .Returns(subscriberId); + + _organizationRepository.GetByIdAsync(subscriberId) + .Returns(mockOrganizationWithoutCustomerId); + + _providerRepository.GetByIdAsync(subscriberId) + .Returns(mockProvider); + + _stripeFacade.GetCustomer(providerCustomerId) + .Returns(customer); + + // Act + var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); + + // Assert + Assert.True(cloudRegionValid); + + await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id); + await _organizationRepository.Received(1).GetByIdAsync(subscriberId); + await _providerRepository.Received(1).GetByIdAsync(subscriberId); + await _stripeFacade.Received(1).GetCustomer(providerCustomerId); + } + + [Fact] + public async Task ValidateCloudRegion_SetupIntentSucceeded_ProviderWithoutGatewayCustomerId_ReturnsFalse() + { + // Arrange + var mockSetupIntent = new SetupIntent { Id = "seti_test" }; + var stripeEvent = CreateMockEvent("evt_test", "setup_intent.succeeded", mockSetupIntent); + var subscriberId = Guid.NewGuid(); + + var mockProviderWithoutCustomerId = new Core.AdminConsole.Entities.Provider.Provider + { + Id = subscriberId, + GatewayCustomerId = null + }; + + _setupIntentCache.GetSubscriberIdForSetupIntent(mockSetupIntent.Id) + .Returns(subscriberId); + + _organizationRepository.GetByIdAsync(subscriberId) + .Returns((Core.AdminConsole.Entities.Organization?)null); + + _providerRepository.GetByIdAsync(subscriberId) + .Returns(mockProviderWithoutCustomerId); + + // Act + var cloudRegionValid = await _stripeEventService.ValidateCloudRegion(stripeEvent); + + // Assert + Assert.False(cloudRegionValid); + + await _setupIntentCache.Received(1).GetSubscriberIdForSetupIntent(mockSetupIntent.Id); + await _organizationRepository.Received(1).GetByIdAsync(subscriberId); + await _providerRepository.Received(1).GetByIdAsync(subscriberId); + await _stripeFacade.DidNotReceive().GetCustomer(Arg.Any()); } #endregion - private static T Copy(T input) + private static Event CreateMockEvent(string id, string type, T dataObject) where T : IStripeEntity { - var copy = (T)Activator.CreateInstance(typeof(T)); - - var properties = input.GetType().GetProperties(); - - foreach (var property in properties) + return new Event { - var value = property.GetValue(input); - copy! - .GetType() - .GetProperty(property.Name)! - .SetValue(copy, value); - } - - return copy; + Id = id, + Type = type, + Data = new EventData + { + Object = (IHasObject)dataObject + } + }; } - private static async Task GetCustomerAsync() - => (await StripeTestEvents.GetAsync(StripeEventType.CustomerUpdated)).Data.Object as Customer; + private static Customer CreateMockCustomer() + { + return new Customer + { + Id = "cus_test", + Metadata = new Dictionary { { "region", "US" } } + }; + } } diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index 0d1f54ecfd..6a7cd7704b 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -11,7 +11,6 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; -using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.Extensions.Logging; @@ -33,7 +32,6 @@ public class SubscriptionUpdatedHandlerTests private readonly IStripeFacade _stripeFacade; private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand; private readonly IUserService _userService; - private readonly IPushNotificationService _pushNotificationService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationEnableCommand _organizationEnableCommand; private readonly IOrganizationDisableCommand _organizationDisableCommand; @@ -42,6 +40,7 @@ public class SubscriptionUpdatedHandlerTests private readonly IProviderRepository _providerRepository; private readonly IProviderService _providerService; private readonly IScheduler _scheduler; + private readonly IPushNotificationAdapter _pushNotificationAdapter; private readonly SubscriptionUpdatedHandler _sut; public SubscriptionUpdatedHandlerTests() @@ -53,7 +52,6 @@ public class SubscriptionUpdatedHandlerTests _organizationSponsorshipRenewCommand = Substitute.For(); _userService = Substitute.For(); _providerService = Substitute.For(); - _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); var schedulerFactory = Substitute.For(); _organizationEnableCommand = Substitute.For(); @@ -64,6 +62,7 @@ public class SubscriptionUpdatedHandlerTests _providerService = Substitute.For(); var logger = Substitute.For>(); _scheduler = Substitute.For(); + _pushNotificationAdapter = Substitute.For(); schedulerFactory.GetScheduler().Returns(_scheduler); @@ -74,7 +73,6 @@ public class SubscriptionUpdatedHandlerTests _stripeFacade, _organizationSponsorshipRenewCommand, _userService, - _pushNotificationService, _organizationRepository, schedulerFactory, _organizationEnableCommand, @@ -83,7 +81,8 @@ public class SubscriptionUpdatedHandlerTests _featureService, _providerRepository, _providerService, - logger); + logger, + _pushNotificationAdapter); } [Fact] @@ -540,8 +539,8 @@ public class SubscriptionUpdatedHandlerTests .EnableAsync(organizationId); await _organizationService.Received(1) .UpdateExpirationDateAsync(organizationId, currentPeriodEnd); - await _pushNotificationService.Received(1) - .PushSyncOrganizationStatusAsync(organization); + await _pushNotificationAdapter.Received(1) + .NotifyEnabledChangedAsync(organization); } [Fact] diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index c22cc239d8..eefda06149 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -71,7 +71,7 @@ public class GetOrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); - sutProvider.GetDependency().Get(organization.Id).Returns((string?)null); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(organization.Id).Returns((string?)null); var response = await sutProvider.Sut.Run(organization); @@ -109,7 +109,7 @@ public class GetOrganizationWarningsQueryTests }); sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); - sutProvider.GetDependency().Get(organization.Id).Returns(setupIntentId); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(organization.Id).Returns(setupIntentId); sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is( options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent { diff --git a/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs index 8b1f915658..72280c4c77 100644 --- a/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs +++ b/test/Core.Test/Billing/Payment/Commands/UpdatePaymentMethodCommandTests.cs @@ -74,7 +74,10 @@ public class UpdatePaymentMethodCommandTests }, NextAction = new SetupIntentNextAction { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits + { + HostedVerificationUrl = "https://example.com" + } }, Status = "requires_action" }; @@ -95,7 +98,7 @@ public class UpdatePaymentMethodCommandTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.False(maskedBankAccount.Verified); + Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl); await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id); } @@ -133,7 +136,10 @@ public class UpdatePaymentMethodCommandTests }, NextAction = new SetupIntentNextAction { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits + { + HostedVerificationUrl = "https://example.com" + } }, Status = "requires_action" }; @@ -154,7 +160,7 @@ public class UpdatePaymentMethodCommandTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.False(maskedBankAccount.Verified); + Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl); await _subscriberService.Received(1).CreateStripeCustomer(organization); @@ -199,7 +205,10 @@ public class UpdatePaymentMethodCommandTests }, NextAction = new SetupIntentNextAction { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits + { + HostedVerificationUrl = "https://example.com" + } }, Status = "requires_action" }; @@ -220,7 +229,7 @@ public class UpdatePaymentMethodCommandTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.False(maskedBankAccount.Verified); + Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl); await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id); await _stripeAdapter.Received(1).CustomerUpdateAsync(customer.Id, Arg.Is(options => diff --git a/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs deleted file mode 100644 index 4be5539cc8..0000000000 --- a/test/Core.Test/Billing/Payment/Commands/VerifyBankAccountCommandTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Caches; -using Bit.Core.Billing.Payment.Commands; -using Bit.Core.Services; -using Bit.Core.Test.Billing.Extensions; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Stripe; -using Xunit; - -namespace Bit.Core.Test.Billing.Payment.Commands; - -public class VerifyBankAccountCommandTests -{ - private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); - private readonly IStripeAdapter _stripeAdapter = Substitute.For(); - private readonly VerifyBankAccountCommand _command; - - public VerifyBankAccountCommandTests() - { - _command = new VerifyBankAccountCommand( - Substitute.For>(), - _setupIntentCache, - _stripeAdapter); - } - - [Fact] - public async Task Run_MakesCorrectInvocations_ReturnsMaskedBankAccount() - { - var organization = new Organization - { - Id = Guid.NewGuid(), - GatewayCustomerId = "cus_123" - }; - - const string setupIntentId = "seti_123"; - - _setupIntentCache.Get(organization.Id).Returns(setupIntentId); - - var setupIntent = new SetupIntent - { - Id = setupIntentId, - PaymentMethodId = "pm_123", - PaymentMethod = - new PaymentMethod - { - Id = "pm_123", - Type = "us_bank_account", - UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } - }, - NextAction = new SetupIntentNextAction - { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() - }, - Status = "requires_action" - }; - - _stripeAdapter.SetupIntentGet(setupIntentId, - Arg.Is(options => options.HasExpansions("payment_method"))).Returns(setupIntent); - - _stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, - Arg.Is(options => options.Customer == organization.GatewayCustomerId)) - .Returns(setupIntent.PaymentMethod); - - var result = await _command.Run(organization, "DESCRIPTOR_CODE"); - - Assert.True(result.IsT0); - var maskedPaymentMethod = result.AsT0; - Assert.True(maskedPaymentMethod.IsT0); - var maskedBankAccount = maskedPaymentMethod.AsT0; - Assert.Equal("Chase", maskedBankAccount.BankName); - Assert.Equal("9999", maskedBankAccount.Last4); - Assert.True(maskedBankAccount.Verified); - - await _stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id, - Arg.Is(options => options.DescriptorCode == "DESCRIPTOR_CODE")); - - await _stripeAdapter.Received(1).CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is( - options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId)); - } -} diff --git a/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs index 39753857d5..21d47f7615 100644 --- a/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs +++ b/test/Core.Test/Billing/Payment/Models/MaskedPaymentMethodTests.cs @@ -13,7 +13,7 @@ public class MaskedPaymentMethodTests { BankName = "Chase", Last4 = "9999", - Verified = true + HostedVerificationUrl = "https://example.com" }; var json = JsonSerializer.Serialize(input); @@ -32,7 +32,7 @@ public class MaskedPaymentMethodTests { BankName = "Chase", Last4 = "9999", - Verified = true + HostedVerificationUrl = "https://example.com" }; var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; diff --git a/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs index 8a4475268d..b6b0d596b3 100644 --- a/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs +++ b/test/Core.Test/Billing/Payment/Queries/GetPaymentMethodQueryTests.cs @@ -108,7 +108,7 @@ public class GetPaymentMethodQueryTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.True(maskedBankAccount.Verified); + Assert.Null(maskedBankAccount.HostedVerificationUrl); } [Fact] @@ -142,7 +142,7 @@ public class GetPaymentMethodQueryTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.True(maskedBankAccount.Verified); + Assert.Null(maskedBankAccount.HostedVerificationUrl); } [Fact] @@ -163,7 +163,7 @@ public class GetPaymentMethodQueryTests Arg.Is(options => options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).Returns(customer); - _setupIntentCache.Get(organization.Id).Returns("seti_123"); + _setupIntentCache.GetSetupIntentIdForSubscriber(organization.Id).Returns("seti_123"); _stripeAdapter .SetupIntentGet("seti_123", @@ -177,7 +177,10 @@ public class GetPaymentMethodQueryTests }, NextAction = new SetupIntentNextAction { - VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits + { + HostedVerificationUrl = "https://example.com" + } }, Status = "requires_action" }); @@ -189,7 +192,7 @@ public class GetPaymentMethodQueryTests var maskedBankAccount = maskedPaymentMethod.AsT0; Assert.Equal("Chase", maskedBankAccount.BankName); Assert.Equal("9999", maskedBankAccount.Last4); - Assert.False(maskedBankAccount.Verified); + Assert.Equal("https://example.com", maskedBankAccount.HostedVerificationUrl); } [Fact] diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 600f9d9be2..de8c6aae19 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -670,7 +670,7 @@ public class SubscriberServiceTests } }; - sutProvider.GetDependency().Get(provider.Id).Returns(setupIntent.Id); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id); sutProvider.GetDependency().SetupIntentGet(setupIntent.Id, Arg.Is(options => options.Expand.Contains("payment_method"))).Returns(setupIntent); @@ -1876,7 +1876,7 @@ public class SubscriberServiceTests PaymentMethodId = "payment_method_id" }; - sutProvider.GetDependency().Get(provider.Id).Returns(setupIntent.Id); + sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id); var stripeAdapter = sutProvider.GetDependency(); diff --git a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs index 961d7cd770..9c46211517 100644 --- a/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs +++ b/test/Core.Test/Platform/Push/Engines/AzureQueuePushEngineTests.cs @@ -651,31 +651,6 @@ public class AzureQueuePushEngineTests ); } - [Fact] - public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() - { - var organization = new Organization - { - Id = Guid.NewGuid(), - Enabled = true, - }; - - var expectedPayload = new JsonObject - { - ["Type"] = 18, - ["Payload"] = new JsonObject - { - ["OrganizationId"] = organization.Id, - ["Enabled"] = organization.Enabled, - }, - }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrganizationStatusAsync(organization), - expectedPayload - ); - } - [Fact] public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse() { diff --git a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs index 9097028370..e0eeeda97d 100644 --- a/test/Core.Test/Platform/Push/Engines/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Engines/PushTestBase.cs @@ -413,21 +413,6 @@ public abstract class PushTestBase ); } - [Fact] - public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() - { - var organization = new Organization - { - Id = Guid.NewGuid(), - Enabled = true, - }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrganizationStatusAsync(organization), - GetPushSyncOrganizationStatusResponsePayload(organization) - ); - } - [Fact] public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse() { From 2986a883eb00cb9039f2dbfd7e4fdf4930112d1b Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Tue, 9 Sep 2025 13:43:14 -0500 Subject: [PATCH 215/326] [PM-25126] Add Bulk Policy Details (#6256) * Added new bulk get for policy details * Query improvements to avoid unnecessary look-ups. --- .../Repositories/IPolicyRepository.cs | 11 + .../Repositories/PolicyRepository.cs | 15 + .../Repositories/PolicyRepository.cs | 90 ++++ .../PolicyDetails_ReadByUserIdsPolicyType.sql | 83 ++++ .../GetPolicyDetailsByUserIdTests.cs | 16 +- ...olicyDetailsByUserIdsAndPolicyTypeTests.cs | 457 ++++++++++++++++++ ..._PolicyDetails_ReadByUserIdsPolicyType.sql | 73 +++ 7 files changed, 737 insertions(+), 8 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdsPolicyType.sql create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdsAndPolicyTypeTests.cs create mode 100644 util/Migrator/DbScripts/2025-08-26_00_PolicyDetails_ReadByUserIdsPolicyType.sql diff --git a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs index 2b46c040bb..9f5c7f3fc4 100644 --- a/src/Core/AdminConsole/Repositories/IPolicyRepository.cs +++ b/src/Core/AdminConsole/Repositories/IPolicyRepository.cs @@ -44,4 +44,15 @@ public interface IPolicyRepository : IRepository /// You probably do not want to call it directly. /// Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType); + + /// + /// Retrieves policy details for a list of users filtered by the specified policy type. + /// + /// A collection of user identifiers for which the policy details are to be fetched. + /// The type of policy for which the details are required. + /// + /// An asynchronous task that returns a collection of objects containing the policy information + /// associated with the specified users and policy type. + /// + Task> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable userIds, PolicyType policyType); } diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs index c93c66c94d..83d5ef6a70 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/PolicyRepository.cs @@ -74,6 +74,21 @@ public class PolicyRepository : Repository, IPolicyRepository } } + public async Task> GetPolicyDetailsByUserIdsAndPolicyType(IEnumerable userIds, PolicyType type) + { + await using var connection = new SqlConnection(ConnectionString); + var results = await connection.QueryAsync( + $"[{Schema}].[PolicyDetails_ReadByUserIdsPolicyType]", + new + { + UserIds = userIds.ToGuidIdArrayTVP(), + PolicyType = (byte)type + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + public async Task> GetPolicyDetailsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) { 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 9d25fd5541..72c277f1d7 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/PolicyRepository.cs @@ -183,4 +183,94 @@ public class PolicyRepository : Repository> GetPolicyDetailsByUserIdsAndPolicyType( + IEnumerable userIds, PolicyType policyType) + { + ArgumentNullException.ThrowIfNull(userIds); + + var userIdsList = userIds.Where(id => id != Guid.Empty).ToList(); + + if (userIdsList.Count == 0) + { + return []; + } + + using var scope = ServiceScopeFactory.CreateScope(); + await using var dbContext = GetDatabaseContext(scope); + + // Get provider relationships + var providerLookup = await (from pu in dbContext.ProviderUsers + join po in dbContext.ProviderOrganizations on pu.ProviderId equals po.ProviderId + where pu.UserId != null && userIdsList.Contains(pu.UserId.Value) + select new { pu.UserId, po.OrganizationId }) + .ToListAsync(); + + // Hashset for lookup + var providerSet = new HashSet<(Guid UserId, Guid OrganizationId)>( + providerLookup.Select(p => (p.UserId!.Value, p.OrganizationId))); + + // Branch 1: Accepted users + var acceptedUsers = await (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 + && p.Type == policyType + && o.Enabled + && o.UsePolicies + && ou.Status != OrganizationUserStatusType.Invited + && ou.UserId != null + && userIdsList.Contains(ou.UserId.Value) + select new + { + OrganizationUserId = ou.Id, + OrganizationId = p.OrganizationId, + PolicyType = p.Type, + PolicyData = p.Data, + OrganizationUserType = ou.Type, + OrganizationUserStatus = ou.Status, + OrganizationUserPermissionsData = ou.Permissions, + UserId = ou.UserId.Value + }).ToListAsync(); + + // Branch 2: Invited users + var invitedUsers = await (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 + join u in dbContext.Users on ou.Email equals u.Email + where p.Enabled + && o.Enabled + && o.UsePolicies + && ou.Status == OrganizationUserStatusType.Invited + && userIdsList.Contains(u.Id) + && p.Type == policyType + select new + { + OrganizationUserId = ou.Id, + OrganizationId = p.OrganizationId, + PolicyType = p.Type, + PolicyData = p.Data, + OrganizationUserType = ou.Type, + OrganizationUserStatus = ou.Status, + OrganizationUserPermissionsData = ou.Permissions, + UserId = u.Id + }).ToListAsync(); + + // Combine results with provder lookup + var allResults = acceptedUsers.Concat(invitedUsers) + .Select(item => new OrganizationPolicyDetails + { + OrganizationUserId = item.OrganizationUserId, + OrganizationId = item.OrganizationId, + PolicyType = item.PolicyType, + PolicyData = item.PolicyData, + OrganizationUserType = item.OrganizationUserType, + OrganizationUserStatus = item.OrganizationUserStatus, + OrganizationUserPermissionsData = item.OrganizationUserPermissionsData, + UserId = item.UserId, + IsProvider = providerSet.Contains((item.UserId, item.OrganizationId)) + }); + + return allResults.ToList(); + } } diff --git a/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdsPolicyType.sql b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdsPolicyType.sql new file mode 100644 index 0000000000..8686802f87 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/PolicyDetails_ReadByUserIdsPolicyType.sql @@ -0,0 +1,83 @@ +CREATE PROCEDURE [dbo].[PolicyDetails_ReadByUserIdsPolicyType] + @UserIds AS [dbo].[GuidIdArray] READONLY, + @PolicyType AS TINYINT +AS +BEGIN + SET NOCOUNT ON; + + WITH AcceptedUsers AS ( + -- Branch 1: Accepted users linked by UserId + SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + OU.[UserId] AS UserId + FROM [dbo].[PolicyView] P + INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN @UserIds UI ON OU.[UserId] = UI.Id -- Direct join with TVP + WHERE + P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND OU.[Status] != 0 -- Accepted users + AND P.[Type] = @PolicyType + ), + InvitedUsers AS ( + -- Branch 2: Invited users matched by email + SELECT + OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + U.[Id] AS UserId + FROM [dbo].[PolicyView] P + INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] -- Join on email + INNER JOIN @UserIds UI ON U.[Id] = UI.Id -- Join with TVP + WHERE + P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND OU.[Status] = 0 -- Invited users only + AND P.[Type] = @PolicyType + ), + AllUsers AS ( + -- Combine both user sets + SELECT * FROM AcceptedUsers + UNION + SELECT * FROM InvitedUsers + ), + ProviderLookup AS ( + -- Pre-calculate provider relationships for all relevant user/org combinations + SELECT DISTINCT + PU.[UserId], + PO.[OrganizationId] + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + INNER JOIN AllUsers AU ON PU.[UserId] = AU.UserId AND PO.[OrganizationId] = AU.OrganizationId + ) + -- Final result with efficient IsProvider lookup + SELECT + AU.OrganizationUserId, + AU.OrganizationId, + AU.PolicyType, + AU.PolicyData, + AU.OrganizationUserType, + AU.OrganizationUserStatus, + AU.OrganizationUserPermissionsData, + AU.UserId, + IIF(PL.UserId IS NOT NULL, 1, 0) AS IsProvider + FROM AllUsers AU + LEFT JOIN ProviderLookup PL + ON AU.UserId = PL.UserId + AND AU.OrganizationId = PL.OrganizationId +END diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs index 07cb82dc02..0a2ddd7387 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdTests.cs @@ -16,7 +16,7 @@ namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRep public class GetPolicyDetailsByUserIdTests { - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_NonInvitedUsers_Works( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -105,7 +105,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Equivalent(new Permissions { ManagePolicies = true }, actualPolicyDetails2.GetOrganizationUserCustomPermissions(), strict: true); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_InvitedUser_Works( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -148,7 +148,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_RevokedConfirmedUser_Works( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -192,7 +192,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_RevokedInvitedUser_DoesntReturnPolicies( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -227,7 +227,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Empty(actualPolicyDetails); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_SetsIsProvider( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -283,7 +283,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Equivalent(expectedPolicyDetails, actualPolicyDetails.Single()); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_IgnoresDisabledOrganizations( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -312,7 +312,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Empty(actualPolicyDetails); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_IgnoresDowngradedOrganizations( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, @@ -342,7 +342,7 @@ public class GetPolicyDetailsByUserIdTests Assert.Empty(actualPolicyDetails); } - [DatabaseTheory, DatabaseData] + [Theory, DatabaseData] public async Task GetPolicyDetailsByUserId_IgnoresDisabledPolicies( IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdsAndPolicyTypeTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdsAndPolicyTypeTests.cs new file mode 100644 index 0000000000..9576967a25 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/PolicyRepository/GetPolicyDetailsByUserIdsAndPolicyTypeTests.cs @@ -0,0 +1,457 @@ +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.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.PolicyRepository; + +public class GetPolicyDetailsByUserIdsAndPolicyTypeTests +{ + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoUsersForAnEnterpriseOrgWithTwoFactorEnabled_WhenUsersHaveBeenConfirmedOrAccepted_ThenShouldReturnCorrectPolicyDetailsAsync( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user1 = await userRepository.CreateAsync(GetDefaultUser()); + + var user2 = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + + var policy = await policyRepository.CreateAsync(new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.TwoFactorAuthentication, + Data = string.Empty, + Enabled = true + }); + + var orgUser1 = await organizationUserRepository.CreateAsync(GetAcceptedOrganizationUser(organization, user1)); + + var orgUser2 = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user2)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user1.Id, user2.Id], + PolicyType.TwoFactorAuthentication); + + // Assert + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + var result1 = resultsList.First(r => r.UserId == user1.Id); + Assert.Equal(orgUser1.Id, result1.OrganizationUserId); + Assert.Equal(organization.Id, result1.OrganizationId); + Assert.Equal(PolicyType.TwoFactorAuthentication, result1.PolicyType); + Assert.Equal(policy.Data, result1.PolicyData); + Assert.Equal(OrganizationUserStatusType.Accepted, result1.OrganizationUserStatus); + + var result2 = resultsList.First(r => r.UserId == user2.Id); + Assert.Equal(orgUser2.Id, result2.OrganizationUserId); + Assert.Equal(organization.Id, result2.OrganizationId); + Assert.Equal(PolicyType.TwoFactorAuthentication, result2.PolicyType); + Assert.Equal(policy.Data, result2.PolicyData); + Assert.Equal(OrganizationUserStatusType.Confirmed, result2.OrganizationUserStatus); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user1, user2]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoUsersForEnterpriseOrgWithMasterPasswordEnabled_WhenUsersHaveBeenInvited_ThenShouldReturnCorrectPolicyDetailsForInvitedUsersAsync( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user1 = await userRepository.CreateAsync(GetDefaultUser()); + + var user2 = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + + _ = await policyRepository.CreateAsync(new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.MasterPassword, + Data = "{\"minComplexity\":4}", + Enabled = true, + }); + + var orgUser1 = await organizationUserRepository.CreateAsync(GetInvitedOrganizationUser(organization, user1)); + + var orgUser2 = await organizationUserRepository.CreateAsync(GetInvitedOrganizationUser(organization, user2)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user1.Id, user2.Id], + PolicyType.MasterPassword); + + // Assert + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + var result1 = resultsList.First(r => r.UserId == user1.Id); + Assert.Equal(orgUser1.Id, result1.OrganizationUserId); + Assert.Equal(organization.Id, result1.OrganizationId); + Assert.Equal(PolicyType.MasterPassword, result1.PolicyType); + Assert.Equal(OrganizationUserStatusType.Invited, result1.OrganizationUserStatus); + + var result2 = resultsList.First(r => r.UserId == user2.Id); + Assert.Equal(orgUser2.Id, result2.OrganizationUserId); + Assert.Equal(organization.Id, result2.OrganizationId); + Assert.Equal(PolicyType.MasterPassword, result2.PolicyType); + Assert.Equal(OrganizationUserStatusType.Invited, result2.OrganizationUserStatus); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user1, user2]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenConfirmedUserEnterpriseOrgWithPolicyEnabled_WhenUserIsAProvider_ThenShouldContainProviderDataAsync( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IProviderUserRepository providerUserRepository, + IProviderOrganizationRepository providerOrganizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.SingleOrg, organization)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user)); + + var provider = await providerRepository.CreateAsync(new Provider + { + Name = "Test Provider", + BusinessName = "Test Provider Business", + BusinessAddress1 = "123 Test St", + BusinessAddress2 = "Suite 456", + BusinessAddress3 = "Floor 7", + BusinessCountry = "US", + BusinessTaxNumber = "123456789", + BillingEmail = $"billing+{Guid.NewGuid()}@example.com" + }); + + await providerUserRepository.CreateAsync(new ProviderUser + { + ProviderId = provider.Id, + UserId = user.Id, + Status = ProviderUserStatusType.Confirmed, + Type = ProviderUserType.ProviderAdmin + }); + + await providerOrganizationRepository.CreateAsync(new ProviderOrganization + { + ProviderId = provider.Id, + OrganizationId = organization.Id + }); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.SingleOrg); + + // Assert + var resultsList = results.ToList(); + Assert.Single(resultsList); + + var result = resultsList.First(); + Assert.True(result.IsProvider); + Assert.Equal(user.Id, result.UserId); + Assert.Equal(organization.Id, result.OrganizationId); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrgWithTwoEnabledPolicies_WhenRequestingTwoFactor_ShouldOnlyReturnInputPolicyType( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + + // Create multiple policies + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization)); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.MasterPassword, organization)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user)); + + // Act - Request only TwoFactorAuthentication policy + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.TwoFactorAuthentication); + + // Assert + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.All(resultsList, r => Assert.Equal(PolicyType.TwoFactorAuthentication, r.PolicyType)); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrg_WhenSendPolicyIsDisabled_ShouldNotReturnDisabledPoliciesAsync( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + _ = await policyRepository.CreateAsync(new Policy + { + OrganizationId = organization.Id, + Type = PolicyType.DisableSend, + Data = "{}", + Enabled = false // Disabled policy + }); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.DisableSend); + + // Assert + Assert.Empty(results); + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenEnterpriseOrgWithPolicies_WhenOrgIsDisabled_ThenShouldNotReturnResults( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = $"billing+{Guid.NewGuid()}@example.com", + Plan = "EnterpriseAnnually", + PlanType = PlanType.EnterpriseAnnually, + Seats = 10, + MaxCollections = 10, + UsePolicies = true, + UseDirectory = true, + UseTotp = true, + Use2fa = true, + UseApi = true, + SelfHost = true, + Enabled = false, // Disabled organization + }); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.RequireSso, organization)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.RequireSso); + + // Assert + Assert.Empty(results); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenOrganization_WhenNotUsingPolicies_ThenShouldNotReturnResults( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = $"billing+{Guid.NewGuid()}@example.com", + Plan = "EnterpriseAnnually", + PlanType = PlanType.EnterpriseAnnually, + Seats = 10, + MaxCollections = 10, + UsePolicies = false, // Not using policies + UseDirectory = true, + UseTotp = true, + Use2fa = true, + UseApi = true, + SelfHost = true, + Enabled = true, + }); + + var policy = await policyRepository.CreateAsync(GetPolicy(PolicyType.PasswordGenerator, organization)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization, user)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.PasswordGenerator); + + // Assert + Assert.Empty(results); + + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([user]); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenOrganization_WhenRequestingWithNoUsers_ShouldReturnEmptyList( + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var organization = await CreateEnterpriseOrgAsync(organizationRepository); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + new List(), + PolicyType.TwoFactorAuthentication); + + // Assert + Assert.Empty(results); + } + + [Theory] + [DatabaseData] + public async Task GetPolicyDetailsByUserIdsAndPolicyType_GivenTwoOrganizations_WhenUserIsAMemberOfBoth_ShouldReturnResultsForBothOrganizations( + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IPolicyRepository policyRepository) + { + // Arrange + var user = await userRepository.CreateAsync(GetDefaultUser()); + + var organization1 = await CreateEnterpriseOrgAsync(organizationRepository); + var organization2 = await CreateEnterpriseOrgAsync(organizationRepository); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization1)); + + _ = await policyRepository.CreateAsync(GetPolicy(PolicyType.TwoFactorAuthentication, organization2)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization1, user)); + + _ = await organizationUserRepository.CreateAsync(GetConfirmedOrganizationUser(organization2, user)); + + // Act + var results = await policyRepository.GetPolicyDetailsByUserIdsAndPolicyType( + [user.Id], + PolicyType.TwoFactorAuthentication); + + // Assert + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + var organizationIds = resultsList.Select(r => r.OrganizationId).ToList(); + Assert.Contains(organization1.Id, organizationIds); + Assert.Contains(organization2.Id, organizationIds); + } + + private static async Task CreateEnterpriseOrgAsync(IOrganizationRepository orgRepo) + { + return await orgRepo.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = $"billing+{Guid.NewGuid()}@example.com", + Plan = "EnterpriseAnnually", + PlanType = PlanType.EnterpriseAnnually, + Seats = 10, + MaxCollections = 10, + UsePolicies = true, + UseDirectory = true, + UseTotp = true, + Use2fa = true, + UseApi = true, + SelfHost = true, + Enabled = true, + }); + } + + private static User GetDefaultUser() => new() + { + Name = $"Test User {Guid.NewGuid()}", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = $"test.api.key.{Guid.NewGuid()}"[..30], + SecurityStamp = Guid.NewGuid().ToString() + }; + + private static OrganizationUser GetAcceptedOrganizationUser(Organization organization, User user) => new() + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User + }; + + private static OrganizationUser GetConfirmedOrganizationUser(Organization organization, User user) => new() + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.User + }; + + private static OrganizationUser GetInvitedOrganizationUser(Organization organization, User user) => new() + { + OrganizationId = organization.Id, + UserId = null, // Invited users don't have UserId + Email = user.Email, + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.User, + }; + + private static Policy GetPolicy(PolicyType policyType, Organization organization) => new() + { + OrganizationId = organization.Id, + Type = policyType, + Data = "{\"test\": \"value\"}", + Enabled = true + }; +} diff --git a/util/Migrator/DbScripts/2025-08-26_00_PolicyDetails_ReadByUserIdsPolicyType.sql b/util/Migrator/DbScripts/2025-08-26_00_PolicyDetails_ReadByUserIdsPolicyType.sql new file mode 100644 index 0000000000..52c335a790 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-26_00_PolicyDetails_ReadByUserIdsPolicyType.sql @@ -0,0 +1,73 @@ +CREATE OR ALTER PROCEDURE [dbo].[PolicyDetails_ReadByUserIdsPolicyType] + @UserIds AS [dbo].[GuidIdArray] READONLY, + @PolicyType AS TINYINT +AS +BEGIN + SET NOCOUNT ON; + + WITH AcceptedUsers AS ( + -- Branch 1: Accepted users linked by UserId + SELECT OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + OU.[UserId] AS UserId + FROM [dbo].[PolicyView] P + INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN @UserIds UI ON OU.[UserId] = UI.Id -- Direct join with TVP + WHERE P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND OU.[Status] != 0 -- Accepted users + AND P.[Type] = @PolicyType), + InvitedUsers AS ( + -- Branch 2: Invited users matched by email + SELECT OU.[Id] AS OrganizationUserId, + P.[OrganizationId], + P.[Type] AS PolicyType, + P.[Data] AS PolicyData, + OU.[Type] AS OrganizationUserType, + OU.[Status] AS OrganizationUserStatus, + OU.[Permissions] AS OrganizationUserPermissionsData, + U.[Id] AS UserId + FROM [dbo].[PolicyView] P + INNER JOIN [dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId] + INNER JOIN [dbo].[OrganizationView] O ON P.[OrganizationId] = O.[Id] + INNER JOIN [dbo].[UserView] U ON U.[Email] = OU.[Email] -- Join on email + INNER JOIN @UserIds UI ON U.[Id] = UI.Id -- Join with TVP + WHERE P.Enabled = 1 + AND O.Enabled = 1 + AND O.UsePolicies = 1 + AND OU.[Status] = 0 -- Invited users only + AND P.[Type] = @PolicyType), + AllUsers AS ( + -- Combine both user sets + SELECT * + FROM AcceptedUsers + UNION + SELECT * + FROM InvitedUsers), + ProviderLookup AS ( + -- Pre-calculate provider relationships for all relevant user/org combinations + SELECT DISTINCT PU.[UserId], + PO.[OrganizationId] + FROM [dbo].[ProviderUserView] PU + INNER JOIN [dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId] + INNER JOIN AllUsers AU ON PU.[UserId] = AU.UserId AND PO.[OrganizationId] = AU.OrganizationId) + -- Final result with efficient IsProvider lookup + SELECT AU.OrganizationUserId, + AU.OrganizationId, + AU.PolicyType, + AU.PolicyData, + AU.OrganizationUserType, + AU.OrganizationUserStatus, + AU.OrganizationUserPermissionsData, + AU.UserId, + IIF(PL.UserId IS NOT NULL, 1, 0) AS IsProvider + FROM AllUsers AU + LEFT JOIN ProviderLookup PL ON AU.UserId = PL.UserId AND AU.OrganizationId = PL.OrganizationId +END From c4f22a45085c6438dd79c1b4080f26ca30ae85d2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 9 Sep 2025 12:30:58 -0700 Subject: [PATCH 216/326] [PM-25381] Add env variables for controlling refresh token lifetimes (#6276) * add env variables for controlling refresh token lifetimes * fix whitespace * added setting for adjusting refresh token expiration policy * format --- src/Core/Settings/GlobalSettings.cs | 12 ++++++++++++ src/Identity/IdentityServer/ApiClient.cs | 14 +++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 638e1477c1..c6c96cffb9 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -468,6 +468,18 @@ public class GlobalSettings : IGlobalSettings public string RedisConnectionString { get; set; } public string CosmosConnectionString { get; set; } public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzM0NTY2NDAwLCJleHAiOjE3NjQ5NzkyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiNjg3OCIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwicHJvZHVjdCI6IkJpdHdhcmRlbiJ9.TYc88W_t2t0F2AJV3rdyKwGyQKrKFriSAzm1tWFNHNR9QizfC-8bliGdT4Wgeie-ynCXs9wWaF-sKC5emg--qS7oe2iIt67Qd88WS53AwgTvAddQRA4NhGB1R7VM8GAikLieSos-DzzwLYRgjZdmcsprItYGSJuY73r-7-F97ta915majBytVxGF966tT9zF1aYk0bA8FS6DcDYkr5f7Nsy8daS_uIUAgNa_agKXtmQPqKujqtUb6rgWEpSp4OcQcG-8Dpd5jHqoIjouGvY-5LTgk5WmLxi_m-1QISjxUJrUm-UGao3_VwV5KFGqYrz8csdTl-HS40ihWcsWnrV0ug"; + /// + /// Global override for sliding refresh token lifetime in seconds. If null, uses the constructor parameter value. + /// + public int? SlidingRefreshTokenLifetimeSeconds { get; set; } + /// + /// Global override for absolute refresh token lifetime in seconds. If null, uses the constructor parameter value. + /// + public int? AbsoluteRefreshTokenLifetimeSeconds { get; set; } + /// + /// Global override for refresh token expiration policy. False = Sliding (default), True = Absolute. + /// + public bool UseAbsoluteRefreshTokenExpiration { get; set; } = false; } public class DataProtectionSettings diff --git a/src/Identity/IdentityServer/ApiClient.cs b/src/Identity/IdentityServer/ApiClient.cs index fa5003e0dc..61b51797c0 100644 --- a/src/Identity/IdentityServer/ApiClient.cs +++ b/src/Identity/IdentityServer/ApiClient.cs @@ -18,10 +18,18 @@ public class ApiClient : Client { ClientId = id; AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode, WebAuthnGrantValidator.GrantType }; - RefreshTokenExpiration = TokenExpiration.Sliding; + + // Use global setting: false = Sliding (default), true = Absolute + RefreshTokenExpiration = globalSettings.IdentityServer.UseAbsoluteRefreshTokenExpiration + ? TokenExpiration.Absolute + : TokenExpiration.Sliding; + RefreshTokenUsage = TokenUsage.ReUse; - SlidingRefreshTokenLifetime = 86400 * refreshTokenSlidingDays; - AbsoluteRefreshTokenLifetime = 0; // forever + + // Use global setting if provided, otherwise use constructor parameter + SlidingRefreshTokenLifetime = globalSettings.IdentityServer.SlidingRefreshTokenLifetimeSeconds ?? (86400 * refreshTokenSlidingDays); + AbsoluteRefreshTokenLifetime = globalSettings.IdentityServer.AbsoluteRefreshTokenLifetimeSeconds ?? 0; // forever + UpdateAccessTokenClaimsOnRefresh = true; AccessTokenLifetime = 3600 * accessTokenLifetimeHours; AllowOfflineAccess = true; From 4f4b35e4bf1034d0fdf07098f5d418753ca4b312 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 16:55:31 -0400 Subject: [PATCH 217/326] [deps] Auth: Update DuoUniversal to 1.3.1 (#5862) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 04dd7781bc..a7db90e892 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -30,7 +30,7 @@ - + From 3283e6c1a645769d80ce00f751d8e274b29376a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:14:44 -0400 Subject: [PATCH 218/326] [deps] Auth: Update webpack to v5.101.3 (#6208) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 48 +++++++++++++-------- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 48 +++++++++++++-------- src/Admin/package.json | 2 +- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index a6db196d48..6c79f70673 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.89.2", "sass-loader": "16.0.5", - "webpack": "5.99.8", + "webpack": "5.101.3", "webpack-cli": "5.1.4" } }, @@ -441,9 +441,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -687,9 +687,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -699,6 +699,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2198,22 +2211,23 @@ } }, "node_modules/webpack": { - "version": "5.99.8", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", - "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -2227,7 +2241,7 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -2317,9 +2331,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", "engines": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 064cf6d656..e62a62c653 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -18,7 +18,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.89.2", "sass-loader": "16.0.5", - "webpack": "5.99.8", + "webpack": "5.101.3", "webpack-cli": "5.1.4" } } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index b3f19c4792..cda0560d64 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -20,7 +20,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.89.2", "sass-loader": "16.0.5", - "webpack": "5.99.8", + "webpack": "5.101.3", "webpack-cli": "5.1.4" } }, @@ -442,9 +442,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -688,9 +688,9 @@ "license": "Apache-2.0" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -700,6 +700,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -2207,22 +2220,23 @@ } }, "node_modules/webpack": { - "version": "5.99.8", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", - "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", + "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -2236,7 +2250,7 @@ "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -2326,9 +2340,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", "engines": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 9076a46239..5ab1cf0815 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.89.2", "sass-loader": "16.0.5", - "webpack": "5.99.8", + "webpack": "5.101.3", "webpack-cli": "5.1.4" } } From 48a262ff1eab5aa61f5c7363fc408aab95356b78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:15:47 -0400 Subject: [PATCH 219/326] [deps] Auth: Update sass to v1.91.0 (#6206) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 6c79f70673..9502ea638d 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.89.2", + "sass": "1.91.0", "sass-loader": "16.0.5", "webpack": "5.101.3", "webpack-cli": "5.1.4" @@ -1873,9 +1873,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.89.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", - "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index e62a62c653..28f40f0d25 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -16,7 +16,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.89.2", + "sass": "1.91.0", "sass-loader": "16.0.5", "webpack": "5.101.3", "webpack-cli": "5.1.4" diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index cda0560d64..aaa5d85aa3 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -18,7 +18,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.89.2", + "sass": "1.91.0", "sass-loader": "16.0.5", "webpack": "5.101.3", "webpack-cli": "5.1.4" @@ -1874,9 +1874,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.89.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", - "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", + "version": "1.91.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 5ab1cf0815..89ee1c5358 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.89.2", + "sass": "1.91.0", "sass-loader": "16.0.5", "webpack": "5.101.3", "webpack-cli": "5.1.4" From 5f76804f476c0ecc629140dd4396f2be4588af5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Wed, 10 Sep 2025 01:00:07 +0200 Subject: [PATCH 220/326] Improve Swagger OperationIDs for AC (#6236) --- .../Controllers/GroupsController.cs | 34 ++++++- .../OrganizationConnectionsController.cs | 8 +- .../OrganizationDomainController.cs | 10 +- ...ationIntegrationConfigurationController.cs | 8 +- .../OrganizationIntegrationController.cs | 8 +- .../OrganizationUsersController.cs | 93 ++++++++++++++++--- .../Controllers/OrganizationsController.cs | 16 +++- .../Controllers/PoliciesController.cs | 2 +- .../ProviderOrganizationsController.cs | 8 +- .../Controllers/ProviderUsersController.cs | 26 +++++- .../Controllers/ProvidersController.cs | 16 +++- .../OrganizationDomainControllerTests.cs | 6 +- .../OrganizationUsersControllerTests.cs | 2 +- 13 files changed, 202 insertions(+), 35 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs index f8e97881cb..4587e54aee 100644 --- a/src/Api/AdminConsole/Controllers/GroupsController.cs +++ b/src/Api/AdminConsole/Controllers/GroupsController.cs @@ -163,7 +163,6 @@ public class GroupsController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model) { if (!await _currentContext.ManageGroups(orgId)) @@ -237,8 +236,14 @@ public class GroupsController : Controller return new GroupResponseModel(group); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + public async Task PostPut(Guid orgId, Guid id, [FromBody] GroupRequestModel model) + { + return await Put(orgId, id, model); + } + [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(string orgId, string id) { var group = await _groupRepository.GetByIdAsync(new Guid(id)); @@ -250,8 +255,14 @@ public class GroupsController : Controller await _deleteGroupCommand.DeleteAsync(group); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDelete(string orgId, string id) + { + await Delete(orgId, id); + } + [HttpDelete("")] - [HttpPost("delete")] public async Task BulkDelete([FromBody] GroupBulkRequestModel model) { var groups = await _groupRepository.GetManyByManyIds(model.Ids); @@ -267,9 +278,15 @@ public class GroupsController : Controller await _deleteGroupCommand.DeleteManyAsync(groups); } + [HttpPost("delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostBulkDelete([FromBody] GroupBulkRequestModel model) + { + await BulkDelete(model); + } + [HttpDelete("{id}/user/{orgUserId}")] - [HttpPost("{id}/delete-user/{orgUserId}")] - public async Task Delete(string orgId, string id, string orgUserId) + public async Task DeleteUser(string orgId, string id, string orgUserId) { var group = await _groupRepository.GetByIdAsync(new Guid(id)); if (group == null || !await _currentContext.ManageGroups(group.OrganizationId)) @@ -279,4 +296,11 @@ public class GroupsController : Controller await _groupService.DeleteUserAsync(group, new Guid(orgUserId)); } + + [HttpPost("{id}/delete-user/{orgUserId}")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDeleteUser(string orgId, string id, string orgUserId) + { + await DeleteUser(orgId, id, orgUserId); + } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs index 79ed2ceabe..776e28d2a3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs @@ -140,7 +140,6 @@ public class OrganizationConnectionsController : Controller } [HttpDelete("{organizationConnectionId}")] - [HttpPost("{organizationConnectionId}/delete")] public async Task DeleteConnection(Guid organizationConnectionId) { var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId); @@ -158,6 +157,13 @@ public class OrganizationConnectionsController : Controller await _deleteOrganizationConnectionCommand.DeleteAsync(connection); } + [HttpPost("{organizationConnectionId}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDeleteConnection(Guid organizationConnectionId) + { + await DeleteConnection(organizationConnectionId); + } + private async Task> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) => await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type); diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index a8882dfaf3..15cfafe240 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -46,7 +46,7 @@ public class OrganizationDomainController : Controller } [HttpGet("{orgId}/domain")] - public async Task> Get(Guid orgId) + public async Task> GetAll(Guid orgId) { await ValidateOrganizationAccessAsync(orgId); @@ -105,7 +105,6 @@ public class OrganizationDomainController : Controller } [HttpDelete("{orgId}/domain/{id}")] - [HttpPost("{orgId}/domain/{id}/remove")] public async Task RemoveDomain(Guid orgId, Guid id) { await ValidateOrganizationAccessAsync(orgId); @@ -119,6 +118,13 @@ public class OrganizationDomainController : Controller await _deleteOrganizationDomainCommand.DeleteAsync(domain); } + [HttpPost("{orgId}/domain/{id}/remove")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostRemoveDomain(Guid orgId, Guid id) + { + await RemoveDomain(orgId, id); + } + [AllowAnonymous] [HttpPost("domain/sso/details")] // must be post to accept email cleanly public async Task GetOrgDomainSsoDetails( diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs index 319fbbe707..ae0f91d355 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs @@ -98,7 +98,6 @@ public class OrganizationIntegrationConfigurationController( } [HttpDelete("{configurationId:guid}")] - [HttpPost("{configurationId:guid}/delete")] public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId) { if (!await HasPermission(organizationId)) @@ -120,6 +119,13 @@ public class OrganizationIntegrationConfigurationController( await integrationConfigurationRepository.DeleteAsync(configuration); } + [HttpPost("{configurationId:guid}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId) + { + await DeleteAsync(organizationId, integrationId, configurationId); + } + private async Task HasPermission(Guid organizationId) { return await currentContext.OrganizationOwner(organizationId); diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs index 7052350c9a..a12492949d 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs @@ -64,7 +64,6 @@ public class OrganizationIntegrationController( } [HttpDelete("{integrationId:guid}")] - [HttpPost("{integrationId:guid}/delete")] public async Task DeleteAsync(Guid organizationId, Guid integrationId) { if (!await HasPermission(organizationId)) @@ -81,6 +80,13 @@ public class OrganizationIntegrationController( await integrationRepository.DeleteAsync(integration); } + [HttpPost("{integrationId:guid}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDeleteAsync(Guid organizationId, Guid integrationId) + { + await DeleteAsync(organizationId, integrationId); + } + private async Task HasPermission(Guid organizationId) { return await currentContext.OrganizationOwner(organizationId); diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 2b464c24e2..5183b59b00 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -167,7 +167,7 @@ public class OrganizationUsersController : Controller } [HttpGet("")] - public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) + public async Task> GetAll(Guid orgId, bool includeGroups = false, bool includeCollections = false) { var request = new OrganizationUserUserDetailsQueryRequest { @@ -360,7 +360,6 @@ public class OrganizationUsersController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] [Authorize] public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model) { @@ -436,6 +435,14 @@ public class OrganizationUsersController : Controller collectionsToSave, groupsToSave); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task PostPut(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model) + { + await Put(orgId, id, model); + } + [HttpPut("{userId}/reset-password-enrollment")] public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model) { @@ -492,7 +499,6 @@ public class OrganizationUsersController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/remove")] [Authorize] public async Task Remove(Guid orgId, Guid id) { @@ -500,8 +506,15 @@ public class OrganizationUsersController : Controller await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value); } + [HttpPost("{id}/remove")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + [Authorize] + public async Task PostRemove(Guid orgId, Guid id) + { + await Remove(orgId, id); + } + [HttpDelete("")] - [HttpPost("remove")] [Authorize] public async Task> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { @@ -511,8 +524,15 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } + [HttpPost("remove")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + [Authorize] + public async Task> PostBulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) + { + return await BulkRemove(orgId, model); + } + [HttpDelete("{id}/delete-account")] - [HttpPost("{id}/delete-account")] [Authorize] public async Task DeleteAccount(Guid orgId, Guid id) { @@ -525,8 +545,15 @@ public class OrganizationUsersController : Controller await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); } + [HttpPost("{id}/delete-account")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + [Authorize] + public async Task PostDeleteAccount(Guid orgId, Guid id) + { + await DeleteAccount(orgId, id); + } + [HttpDelete("delete-account")] - [HttpPost("delete-account")] [Authorize] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { @@ -542,7 +569,14 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } - [HttpPatch("{id}/revoke")] + [HttpPost("delete-account")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + [Authorize] + public async Task> PostBulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) + { + return await BulkDeleteAccount(orgId, model); + } + [HttpPut("{id}/revoke")] [Authorize] public async Task RevokeAsync(Guid orgId, Guid id) @@ -550,7 +584,14 @@ public class OrganizationUsersController : Controller await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync); } - [HttpPatch("revoke")] + [HttpPatch("{id}/revoke")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task PatchRevokeAsync(Guid orgId, Guid id) + { + await RevokeAsync(orgId, id); + } + [HttpPut("revoke")] [Authorize] public async Task> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) @@ -558,7 +599,14 @@ public class OrganizationUsersController : Controller return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync); } - [HttpPatch("{id}/restore")] + [HttpPatch("revoke")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task> PatchBulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) + { + return await BulkRevokeAsync(orgId, model); + } + [HttpPut("{id}/restore")] [Authorize] public async Task RestoreAsync(Guid orgId, Guid id) @@ -566,7 +614,14 @@ public class OrganizationUsersController : Controller await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId)); } - [HttpPatch("restore")] + [HttpPatch("{id}/restore")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task PatchRestoreAsync(Guid orgId, Guid id) + { + await RestoreAsync(orgId, id); + } + [HttpPut("restore")] [Authorize] public async Task> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) @@ -574,7 +629,14 @@ public class OrganizationUsersController : Controller return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService)); } - [HttpPatch("enable-secrets-manager")] + [HttpPatch("restore")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task> PatchBulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) + { + return await BulkRestoreAsync(orgId, model); + } + [HttpPut("enable-secrets-manager")] [Authorize] public async Task BulkEnableSecretsManagerAsync(Guid orgId, @@ -607,6 +669,15 @@ public class OrganizationUsersController : Controller await _organizationUserRepository.ReplaceManyAsync(orgUsers); } + [HttpPatch("enable-secrets-manager")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + [Authorize] + public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId, + [FromBody] OrganizationUserBulkRequestModel model) + { + await BulkEnableSecretsManagerAsync(orgId, model); + } + private async Task RestoreOrRevokeUserAsync( Guid orgId, Guid id, diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 17e6a60cd9..590895665d 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -225,7 +225,6 @@ public class OrganizationsController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(string id, [FromBody] OrganizationUpdateRequestModel model) { var orgIdGuid = new Guid(id); @@ -252,6 +251,13 @@ public class OrganizationsController : Controller return new OrganizationResponseModel(organization, plan); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + public async Task PostPut(string id, [FromBody] OrganizationUpdateRequestModel model) + { + return await Put(id, model); + } + [HttpPost("{id}/storage")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostStorage(string id, [FromBody] StorageRequestModel model) @@ -291,7 +297,6 @@ public class OrganizationsController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(string id, [FromBody] SecretVerificationRequestModel model) { var orgIdGuid = new Guid(id); @@ -334,6 +339,13 @@ public class OrganizationsController : Controller await _organizationDeleteCommand.DeleteAsync(organization); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model) + { + await Delete(id, model); + } + [HttpPost("{id}/delete-recover-token")] [AllowAnonymous] public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model) diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index a80546e2f5..88777f1c30 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -90,7 +90,7 @@ public class PoliciesController : Controller } [HttpGet("")] - public async Task> Get(string orgId) + public async Task> GetAll(string orgId) { var orgIdGuid = new Guid(orgId); if (!await _currentContext.ManagePolicies(orgIdGuid)) diff --git a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs index f68b036be4..11d302ff86 100644 --- a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs @@ -93,7 +93,6 @@ public class ProviderOrganizationsController : Controller } [HttpDelete("{id:guid}")] - [HttpPost("{id:guid}/delete")] public async Task Delete(Guid providerId, Guid id) { if (!_currentContext.ManageProviderOrganizations(providerId)) @@ -112,4 +111,11 @@ public class ProviderOrganizationsController : Controller providerOrganization, organization); } + + [HttpPost("{id:guid}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDelete(Guid providerId, Guid id) + { + await Delete(providerId, id); + } } diff --git a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs index b89f553325..dcf9492605 100644 --- a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs @@ -49,7 +49,7 @@ public class ProviderUsersController : Controller } [HttpGet("")] - public async Task> Get(Guid providerId) + public async Task> GetAll(Guid providerId) { if (!_currentContext.ProviderManageUsers(providerId)) { @@ -155,7 +155,6 @@ public class ProviderUsersController : Controller } [HttpPut("{id:guid}")] - [HttpPost("{id:guid}")] public async Task Put(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model) { if (!_currentContext.ProviderManageUsers(providerId)) @@ -173,8 +172,14 @@ public class ProviderUsersController : Controller await _providerService.SaveUserAsync(model.ToProviderUser(providerUser), userId.Value); } + [HttpPost("{id:guid}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + public async Task PostPut(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model) + { + await Put(providerId, id, model); + } + [HttpDelete("{id:guid}")] - [HttpPost("{id:guid}/delete")] public async Task Delete(Guid providerId, Guid id) { if (!_currentContext.ProviderManageUsers(providerId)) @@ -186,8 +191,14 @@ public class ProviderUsersController : Controller await _providerService.DeleteUsersAsync(providerId, new[] { id }, userId.Value); } + [HttpPost("{id:guid}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDelete(Guid providerId, Guid id) + { + await Delete(providerId, id); + } + [HttpDelete("")] - [HttpPost("delete")] public async Task> BulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model) { if (!_currentContext.ProviderManageUsers(providerId)) @@ -200,4 +211,11 @@ public class ProviderUsersController : Controller return new ListResponseModel(result.Select(r => new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2))); } + + [HttpPost("delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task> PostBulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model) + { + return await BulkDelete(providerId, model); + } } diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs index d8bda2ca18..a1815fd3bf 100644 --- a/src/Api/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs @@ -53,7 +53,6 @@ public class ProvidersController : Controller } [HttpPut("{id:guid}")] - [HttpPost("{id:guid}")] public async Task Put(Guid id, [FromBody] ProviderUpdateRequestModel model) { if (!_currentContext.ProviderProviderAdmin(id)) @@ -71,6 +70,13 @@ public class ProvidersController : Controller return new ProviderResponseModel(provider); } + [HttpPost("{id:guid}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead")] + public async Task PostPut(Guid id, [FromBody] ProviderUpdateRequestModel model) + { + return await Put(id, model); + } + [HttpPost("{id:guid}/setup")] public async Task Setup(Guid id, [FromBody] ProviderSetupRequestModel model) { @@ -120,7 +126,6 @@ public class ProvidersController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(Guid id) { if (!_currentContext.ProviderProviderAdmin(id)) @@ -142,4 +147,11 @@ public class ProvidersController : Controller await _providerService.DeleteAsync(provider); } + + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead")] + public async Task PostDelete(Guid id) + { + await Delete(id); + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs index 352f089db7..f81221c605 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationDomainControllerTests.cs @@ -28,7 +28,7 @@ public class OrganizationDomainControllerTests { sutProvider.GetDependency().ManageSso(orgId).Returns(false); - var requestAction = async () => await sutProvider.Sut.Get(orgId); + var requestAction = async () => await sutProvider.Sut.GetAll(orgId); await Assert.ThrowsAsync(requestAction); } @@ -40,7 +40,7 @@ public class OrganizationDomainControllerTests sutProvider.GetDependency().ManageSso(orgId).Returns(true); sutProvider.GetDependency().GetByIdAsync(orgId).ReturnsNull(); - var requestAction = async () => await sutProvider.Sut.Get(orgId); + var requestAction = async () => await sutProvider.Sut.GetAll(orgId); await Assert.ThrowsAsync(requestAction); } @@ -64,7 +64,7 @@ public class OrganizationDomainControllerTests } }); - var result = await sutProvider.Sut.Get(orgId); + var result = await sutProvider.Sut.GetAll(orgId); Assert.IsType>(result); Assert.Equal(orgId, result.Data.Select(x => x.OrganizationId).FirstOrDefault()); diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index cc480d1dcb..2c45385002 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -271,7 +271,7 @@ public class OrganizationUsersControllerTests SutProvider sutProvider) { GetMany_Setup(organizationAbility, organizationUsers, sutProvider); - var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false); + var response = await sutProvider.Sut.GetAll(organizationAbility.Id, false, false); Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id))); } From 52045b89fada48234ffe2630d3d84fa6ec88e96f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:55:21 -0400 Subject: [PATCH 221/326] [deps]: Lock file maintenance (#5876) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 126 ++++++++---------- src/Admin/package-lock.json | 126 ++++++++---------- src/Core/MailTemplates/Mjml/package-lock.json | 48 +++---- 3 files changed, 140 insertions(+), 160 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 9502ea638d..aeefbd69d7 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -34,18 +34,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -58,20 +54,10 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -80,16 +66,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -455,13 +441,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.10.0" } }, "node_modules/@webassemblyjs/ast": { @@ -794,9 +780,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -814,8 +800,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -834,9 +820,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -988,16 +974,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -1120,9 +1106,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -1254,9 +1240,9 @@ } }, "node_modules/immutable": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", - "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "dev": true, "license": "MIT" }, @@ -1541,9 +1527,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, @@ -1648,9 +1634,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1668,7 +1654,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2074,24 +2060,28 @@ } }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.39.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", - "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2152,9 +2142,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index aaa5d85aa3..2e3a335598 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -35,18 +35,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -59,20 +55,10 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -81,16 +67,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -456,13 +442,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.10.0" } }, "node_modules/@webassemblyjs/ast": { @@ -795,9 +781,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -815,8 +801,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -835,9 +821,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -989,16 +975,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.215", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz", + "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -1121,9 +1107,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -1255,9 +1241,9 @@ } }, "node_modules/immutable": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", - "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", "dev": true, "license": "MIT" }, @@ -1542,9 +1528,9 @@ "optional": true }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz", + "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==", "dev": true, "license": "MIT" }, @@ -1649,9 +1635,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -1669,7 +1655,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2075,24 +2061,28 @@ } }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.39.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", - "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2161,9 +2151,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, diff --git a/src/Core/MailTemplates/Mjml/package-lock.json b/src/Core/MailTemplates/Mjml/package-lock.json index 30e1e3568c..a78405676f 100644 --- a/src/Core/MailTemplates/Mjml/package-lock.json +++ b/src/Core/MailTemplates/Mjml/package-lock.json @@ -18,9 +18,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", - "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -78,9 +78,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -90,9 +90,9 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -139,9 +139,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -386,9 +386,9 @@ } }, "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -402,9 +402,9 @@ } }, "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -1415,9 +1415,9 @@ } }, "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1758,9 +1758,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" From d43b00dad9f37432fd1f72139380e95b6f3e15d4 Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Wed, 10 Sep 2025 10:13:04 -0400 Subject: [PATCH 222/326] [PM-24279] Add vnext policy endpoint (#6253) --- .../Controllers/PoliciesController.cs | 20 +- .../Models/Request/SavePolicyRequest.cs | 61 ++++ .../Organizations/PolicyResponseModel.cs | 4 + .../Policies/IPostSavePolicySideEffect.cs | 10 + .../Policies/ISavePolicyCommand.cs | 6 + .../Implementations/SavePolicyCommand.cs | 45 ++- .../Policies/Models/EmptyMetadataModel.cs | 6 + .../Policies/Models/IPolicyMetadataModel.cs | 6 + .../OrganizationModelOwnershipPolicyModel.cs | 16 + .../Policies/Models/SavePolicyModel.cs | 8 + .../PolicyServiceCollectionExtensions.cs | 8 +- ...rganizationDataOwnershipPolicyValidator.cs | 54 ++-- .../OrganizationPolicyValidator.cs | 25 +- .../Controllers/PoliciesControllerTests.cs | 214 +++++++++++++ .../Models/Request/SavePolicyRequestTests.cs | 303 ++++++++++++++++++ .../AutoFixture/PolicyUpdateFixtures.cs | 2 +- ...zationDataOwnershipPolicyValidatorTests.cs | 154 +++++---- .../OrganizationPolicyValidatorTests.cs | 18 +- .../Policies/SavePolicyCommandTests.cs | 84 ++++- 19 files changed, 908 insertions(+), 136 deletions(-) create mode 100644 src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs create mode 100644 test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs create mode 100644 test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 88777f1c30..ce92321833 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -1,10 +1,13 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Api.AdminConsole.Authorization; +using Bit.Api.AdminConsole.Authorization.Requirements; using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -30,7 +33,6 @@ namespace Bit.Api.AdminConsole.Controllers; public class PoliciesController : Controller { private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery; private readonly IOrganizationRepository _organizationRepository; @@ -49,7 +51,6 @@ public class PoliciesController : Controller GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IFeatureService featureService, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IOrganizationRepository organizationRepository, ISavePolicyCommand savePolicyCommand) @@ -63,7 +64,6 @@ public class PoliciesController : Controller "OrganizationServiceDataProtector"); _organizationRepository = organizationRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _featureService = featureService; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; _savePolicyCommand = savePolicyCommand; } @@ -212,4 +212,18 @@ public class PoliciesController : Controller var policy = await _savePolicyCommand.SaveAsync(policyUpdate); return new PolicyResponseModel(policy); } + + + [HttpPut("{type}/vnext")] + [RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)] + [Authorize] + public async Task PutVNext(Guid orgId, [FromBody] SavePolicyRequest model) + { + var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext); + + var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest); + + return new PolicyResponseModel(policy); + } + } diff --git a/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs new file mode 100644 index 0000000000..fcdc49882b --- /dev/null +++ b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Context; +using Bit.Core.Utilities; + +namespace Bit.Api.AdminConsole.Models.Request; + +public class SavePolicyRequest +{ + [Required] + public PolicyRequestModel Policy { get; set; } = null!; + + public Dictionary? Metadata { get; set; } + + public async Task ToSavePolicyModelAsync(Guid organizationId, ICurrentContext currentContext) + { + var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId)); + + var updatedPolicy = new PolicyUpdate() + { + Type = Policy.Type!.Value, + OrganizationId = organizationId, + Data = Policy.Data != null ? JsonSerializer.Serialize(Policy.Data) : null, + Enabled = Policy.Enabled.GetValueOrDefault(), + }; + + var metadata = MapToPolicyMetadata(); + + return new SavePolicyModel(updatedPolicy, performedBy, metadata); + } + + private IPolicyMetadataModel MapToPolicyMetadata() + { + if (Metadata == null) + { + return new EmptyMetadataModel(); + } + + return Policy?.Type switch + { + PolicyType.OrganizationDataOwnership => MapToPolicyMetadata(), + _ => new EmptyMetadataModel() + }; + } + + private IPolicyMetadataModel MapToPolicyMetadata() where T : IPolicyMetadataModel, new() + { + try + { + var json = JsonSerializer.Serialize(Metadata); + return CoreHelpers.LoadClassFromJsonData(json); + } + catch + { + return new EmptyMetadataModel(); + } + } +} diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs index 9feafce70c..81ca801308 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs @@ -10,6 +10,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations; public class PolicyResponseModel : ResponseModel { + public PolicyResponseModel() : base("policy") + { + } + public PolicyResponseModel(Policy policy, string obj = "policy") : base(obj) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs new file mode 100644 index 0000000000..e90945d12d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/IPostSavePolicySideEffect.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; + +public interface IPostSavePolicySideEffect +{ + public Task ExecuteSideEffectsAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy, + Policy? previousPolicyState); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs index 6ca842686e..73278d77d2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/ISavePolicyCommand.cs @@ -6,4 +6,10 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies; public interface ISavePolicyCommand { Task SaveAsync(PolicyUpdate policy); + + /// + /// FIXME: this is a first pass at implementing side effects after the policy has been saved, which was not supported by the validator pattern. + /// However, this needs to be implemented in a policy-agnostic way rather than building out switch statements in the command itself. + /// + Task VNextSaveAsync(SavePolicyModel policyRequest); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs index 71212aaf4c..e2bca930d1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Implementations/SavePolicyCommand.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.Repositories; @@ -17,18 +15,20 @@ public class SavePolicyCommand : ISavePolicyCommand private readonly IPolicyRepository _policyRepository; private readonly IReadOnlyDictionary _policyValidators; private readonly TimeProvider _timeProvider; + private readonly IPostSavePolicySideEffect _postSavePolicySideEffect; - public SavePolicyCommand( - IApplicationCacheService applicationCacheService, + public SavePolicyCommand(IApplicationCacheService applicationCacheService, IEventService eventService, IPolicyRepository policyRepository, IEnumerable policyValidators, - TimeProvider timeProvider) + TimeProvider timeProvider, + IPostSavePolicySideEffect postSavePolicySideEffect) { _applicationCacheService = applicationCacheService; _eventService = eventService; _policyRepository = policyRepository; _timeProvider = timeProvider; + _postSavePolicySideEffect = postSavePolicySideEffect; var policyValidatorsDict = new Dictionary(); foreach (var policyValidator in policyValidators) @@ -78,12 +78,28 @@ public class SavePolicyCommand : ISavePolicyCommand return policy; } + public async Task VNextSaveAsync(SavePolicyModel policyRequest) + { + var (_, currentPolicy) = await GetCurrentPolicyStateAsync(policyRequest.PolicyUpdate); + + var policy = await SaveAsync(policyRequest.PolicyUpdate); + + await ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(policyRequest, policy, currentPolicy); + + return policy; + } + + private async Task ExecutePostPolicySaveSideEffectsForSupportedPoliciesAsync(SavePolicyModel policyRequest, Policy postUpdatedPolicy, Policy? previousPolicyState) + { + if (postUpdatedPolicy.Type == PolicyType.OrganizationDataOwnership) + { + await _postSavePolicySideEffect.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + } + } + private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate) { - var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); - // Note: policies may be missing from this dict if they have never been enabled - var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); - var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type); + var (savedPoliciesDict, currentPolicy) = await GetCurrentPolicyStateAsync(policyUpdate); // If enabling this policy - check that all policy requirements are satisfied if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled) @@ -127,4 +143,13 @@ public class SavePolicyCommand : ISavePolicyCommand // Run side effects await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); } + + private async Task<(Dictionary savedPoliciesDict, Policy? currentPolicy)> GetCurrentPolicyStateAsync(PolicyUpdate policyUpdate) + { + var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId); + // Note: policies may be missing from this dict if they have never been enabled + var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type); + var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type); + return (savedPoliciesDict, currentPolicy); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs new file mode 100644 index 0000000000..0c086ac575 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/EmptyMetadataModel.cs @@ -0,0 +1,6 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public record EmptyMetadataModel : IPolicyMetadataModel +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs new file mode 100644 index 0000000000..5331524a1d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/IPolicyMetadataModel.cs @@ -0,0 +1,6 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public interface IPolicyMetadataModel +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs new file mode 100644 index 0000000000..0ff9200d8f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/OrganizationModelOwnershipPolicyModel.cs @@ -0,0 +1,16 @@ + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public class OrganizationModelOwnershipPolicyModel : IPolicyMetadataModel +{ + public OrganizationModelOwnershipPolicyModel() + { + } + + public OrganizationModelOwnershipPolicyModel(string? defaultUserCollectionName) + { + DefaultUserCollectionName = defaultUserCollectionName; + } + + public string? DefaultUserCollectionName { get; set; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs new file mode 100644 index 0000000000..7c8d5126e8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/Models/SavePolicyModel.cs @@ -0,0 +1,8 @@ + +using Bit.Core.AdminConsole.Models.Data; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; + +public record SavePolicyModel(PolicyUpdate PolicyUpdate, IActingUser? PerformedBy, IPolicyMetadataModel Metadata) +{ +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 12dd3f973d..5433d70410 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static class PolicyServiceCollectionExtensions services.AddPolicyValidators(); services.AddPolicyRequirements(); + services.AddPolicySideEffects(); } private static void AddPolicyValidators(this IServiceCollection services) @@ -27,8 +28,11 @@ public static class PolicyServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - // This validator will be hooked up in https://bitwarden.atlassian.net/browse/PM-24279. - // services.AddScoped(); + } + + private static void AddPolicySideEffects(this IServiceCollection services) + { + services.AddScoped(); } private static void AddPolicyRequirements(this IServiceCollection services) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs index 2471bda647..f4ef6021a7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidator.cs @@ -1,44 +1,55 @@ -#nullable enable - + 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.Repositories; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; 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, - ILogger logger) - : OrganizationPolicyValidator(policyRepository, factories) + IFeatureService featureService) + : OrganizationPolicyValidator(policyRepository, factories), IPostSavePolicySideEffect { - public override PolicyType Type => PolicyType.OrganizationDataOwnership; - - public override IEnumerable RequiredPolicies => []; - - public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(""); - - public override async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) + public async Task ExecuteSideEffectsAsync( + SavePolicyModel policyRequest, + Policy postUpdatedPolicy, + Policy? previousPolicyState) { if (!featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) { return; } - if (currentPolicy?.Enabled != true && policyUpdate.Enabled) + if (policyRequest.Metadata is not OrganizationModelOwnershipPolicyModel metadata) { - await UpsertDefaultCollectionsForUsersAsync(policyUpdate); + return; + } + + if (string.IsNullOrWhiteSpace(metadata.DefaultUserCollectionName)) + { + return; + } + + var isFirstTimeEnabled = postUpdatedPolicy.Enabled && previousPolicyState == null; + var reEnabled = previousPolicyState?.Enabled == false + && postUpdatedPolicy.Enabled; + + if (isFirstTimeEnabled || reEnabled) + { + await UpsertDefaultCollectionsForUsersAsync(policyRequest.PolicyUpdate, metadata.DefaultUserCollectionName); } } - private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate) + private async Task UpsertDefaultCollectionsForUsersAsync(PolicyUpdate policyUpdate, string defaultCollectionName) { var requirements = await GetUserPolicyRequirementsByOrganizationIdAsync(policyUpdate.OrganizationId, policyUpdate.Type); @@ -49,20 +60,13 @@ public class OrganizationDataOwnershipPolicyValidator( if (!userOrgIds.Any()) { - logger.LogError("No UserOrganizationIds found for {OrganizationId}", policyUpdate.OrganizationId); return; } await collectionRepository.UpsertDefaultCollectionsAsync( policyUpdate.OrganizationId, userOrgIds, - GetDefaultUserCollectionName()); + defaultCollectionName); } - private static string GetDefaultUserCollectionName() - { - // TODO: https://bitwarden.atlassian.net/browse/PM-24279 - const string temporaryPlaceHolderValue = "Default"; - return temporaryPlaceHolderValue; - } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs index 33667b829c..15a0b4bb54 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidator.cs @@ -1,17 +1,16 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; -public abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable> factories) : IPolicyValidator + +/// +/// Please do not use 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 abstract class OrganizationPolicyValidator(IPolicyRepository policyRepository, IEnumerable> factories) { - public abstract PolicyType Type { get; } - - public abstract IEnumerable RequiredPolicies { get; } - protected async Task> GetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement { var factory = factories.OfType>().SingleOrDefault(); @@ -36,14 +35,4 @@ public abstract class OrganizationPolicyValidator(IPolicyRepository policyReposi return requirements; } - - public abstract Task OnSaveSideEffectsAsync( - PolicyUpdate policyUpdate, - Policy? currentPolicy - ); - - public abstract Task ValidateAsync( - PolicyUpdate policyUpdate, - Policy? currentPolicy - ); } diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs new file mode 100644 index 0000000000..1efc2f843d --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/PoliciesControllerTests.cs @@ -0,0 +1,214 @@ +using System.Net; +using System.Text.Json; +using Bit.Api.AdminConsole.Models.Request; +using Bit.Api.AdminConsole.Models.Response.Organizations; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.Helpers; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class PoliciesControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly LoginHelper _loginHelper; + + private Organization _organization = null!; + private string _ownerEmail = null!; + + public PoliciesControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _factory.SubstituteService(featureService => + { + featureService + .IsEnabled("pm-19467-create-default-location") + .Returns(true); + }); + _client = factory.CreateClient(); + _loginHelper = new LoginHelper(_factory, _client); + } + + public async Task InitializeAsync() + { + _ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_ownerEmail); + + (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually, + ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); + + await _loginHelper.LoginAsync(_ownerEmail); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Fact] + public async Task PutVNext_OrganizationDataOwnershipPolicy_Success() + { + // Arrange + const PolicyType policyType = PolicyType.OrganizationDataOwnership; + + const string defaultCollectionName = "Test Default Collection"; + var request = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = policyType, + Enabled = true, + }, + Metadata = new Dictionary + { + { "defaultUserCollectionName", defaultCollectionName } + } + }; + + var (_, admin) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.Admin); + + var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.User); + + // Act + var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext", + JsonContent.Create(request)); + + // Assert + await AssertResponse(); + + await AssertPolicy(); + + await AssertDefaultCollectionCreatedOnlyForUserTypeAsync(); + return; + + async Task AssertDefaultCollectionCreatedOnlyForUserTypeAsync() + { + var collectionRepository = _factory.GetService(); + await AssertUserExpectations(collectionRepository); + await AssertAdminExpectations(collectionRepository); + } + + async Task AssertUserExpectations(ICollectionRepository collectionRepository) + { + var collections = await collectionRepository.GetManyByUserIdAsync(user.UserId!.Value); + var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName); + Assert.NotNull(defaultCollection); + Assert.Equal(_organization.Id, defaultCollection.OrganizationId); + } + + async Task AssertAdminExpectations(ICollectionRepository collectionRepository) + { + var collections = await collectionRepository.GetManyByUserIdAsync(admin.UserId!.Value); + var defaultCollection = collections.FirstOrDefault(c => c.Name == defaultCollectionName); + Assert.Null(defaultCollection); + } + + async Task AssertResponse() + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadFromJsonAsync(); + + Assert.True(content.Enabled); + Assert.Equal(policyType, content.Type); + Assert.Equal(_organization.Id, content.OrganizationId); + } + + async Task AssertPolicy() + { + var policyRepository = _factory.GetService(); + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + + Assert.NotNull(policy); + Assert.True(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.Null(policy.Data); + Assert.Equal(_organization.Id, policy.OrganizationId); + } + } + + [Fact] + public async Task PutVNext_MasterPasswordPolicy_Success() + { + // Arrange + var policyType = PolicyType.MasterPassword; + var request = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = policyType, + Enabled = true, + Data = new Dictionary + { + { "minComplexity", 10 }, + { "minLength", 12 }, + { "requireUpper", true }, + { "requireLower", false }, + { "requireNumbers", true }, + { "requireSpecial", false }, + { "enforceOnLogin", true } + } + } + }; + + // Act + var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}/vnext", + JsonContent.Create(request)); + + // Assert + await AssertResponse(); + + await AssertPolicyDataForMasterPasswordPolicy(); + return; + + async Task AssertPolicyDataForMasterPasswordPolicy() + { + var policyRepository = _factory.GetService(); + var policy = await policyRepository.GetByOrganizationIdTypeAsync(_organization.Id, policyType); + + AssertPolicy(policy); + AssertMasterPasswordPolicyData(policy); + } + + async Task AssertResponse() + { + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadFromJsonAsync(); + + Assert.True(content.Enabled); + Assert.Equal(policyType, content.Type); + Assert.Equal(_organization.Id, content.OrganizationId); + } + + void AssertPolicy(Policy policy) + { + Assert.NotNull(policy); + Assert.True(policy.Enabled); + Assert.Equal(policyType, policy.Type); + Assert.Equal(_organization.Id, policy.OrganizationId); + Assert.NotNull(policy.Data); + } + + void AssertMasterPasswordPolicyData(Policy policy) + { + var resultData = policy.GetDataModel(); + + var json = JsonSerializer.Serialize(request.Policy.Data); + var expectedData = JsonSerializer.Deserialize(json); + AssertHelper.AssertPropertyEqual(resultData, expectedData); + } + } + +} diff --git a/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs b/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs new file mode 100644 index 0000000000..057680425a --- /dev/null +++ b/test/Api.Test/AdminConsole/Models/Request/SavePolicyRequestTests.cs @@ -0,0 +1,303 @@ + +using System.Text.Json; +using Bit.Api.AdminConsole.Models.Request; +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; +using Bit.Core.Context; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Models.Request; + +[SutProviderCustomize] +public class SavePolicyRequestTests +{ + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithValidData_ReturnsCorrectSavePolicyModel( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var testData = new Dictionary { { "test", "value" } }; + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.TwoFactorAuthentication, + Enabled = true, + Data = testData + }, + Metadata = new Dictionary() + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Equal(PolicyType.TwoFactorAuthentication, result.PolicyUpdate.Type); + Assert.Equal(organizationId, result.PolicyUpdate.OrganizationId); + Assert.True(result.PolicyUpdate.Enabled); + Assert.NotNull(result.PolicyUpdate.Data); + + var deserializedData = JsonSerializer.Deserialize>(result.PolicyUpdate.Data); + Assert.Equal("value", deserializedData["test"].ToString()); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider); + + Assert.IsType(result.Metadata); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithNullData_HandlesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(false); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.SingleOrg, + Enabled = false, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Null(result.PolicyUpdate.Data); + Assert.False(result.PolicyUpdate.Enabled); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.False(result!.PerformedBy.IsOrganizationOwnerOrProvider); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_WithNonOrganizationOwner_HandlesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.SingleOrg, + Enabled = false, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.Null(result.PolicyUpdate.Data); + Assert.False(result.PolicyUpdate.Enabled); + + Assert.Equal(userId, result!.PerformedBy.UserId); + Assert.True(result!.PerformedBy.IsOrganizationOwnerOrProvider); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithValidMetadata_ReturnsCorrectMetadata( + Guid organizationId, + Guid userId, + string defaultCollectionName) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = new Dictionary + { + { "defaultUserCollectionName", defaultCollectionName } + } + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.IsType(result.Metadata); + var metadata = (OrganizationModelOwnershipPolicyModel)result.Metadata; + Assert.Equal(defaultCollectionName, metadata.DefaultUserCollectionName); + } + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_OrganizationDataOwnership_WithNullMetadata_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = null + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + private static readonly Dictionary _complexData = new Dictionary + { + { "stringValue", "test" }, + { "numberValue", 42 }, + { "boolValue", true }, + { "arrayValue", new[] { "item1", "item2" } }, + { "nestedObject", new Dictionary { { "nested", "value" } } } + }; + + [Theory, BitAutoData] + public async Task ToSavePolicyModelAsync_ComplexData_SerializesCorrectly( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.ResetPassword, + Enabled = true, + Data = _complexData + }, + Metadata = new Dictionary() + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + var deserializedData = JsonSerializer.Deserialize>(result.PolicyUpdate.Data); + Assert.Equal("test", deserializedData["stringValue"].GetString()); + Assert.Equal(42, deserializedData["numberValue"].GetInt32()); + Assert.True(deserializedData["boolValue"].GetBoolean()); + Assert.Equal(2, deserializedData["arrayValue"].GetArrayLength()); + var array = deserializedData["arrayValue"].EnumerateArray() + .Select(e => e.GetString()) + .ToArray(); + Assert.Contains("item1", array); + Assert.Contains("item2", array); + Assert.True(deserializedData["nestedObject"].TryGetProperty("nested", out var nestedValue)); + Assert.Equal("value", nestedValue.GetString()); + } + + + [Theory, BitAutoData] + public async Task MapToPolicyMetadata_UnknownPolicyType_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.MaximumVaultTimeout, + Enabled = true, + Data = null + }, + Metadata = new Dictionary + { + { "someProperty", "someValue" } + } + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + [Theory, BitAutoData] + public async Task MapToPolicyMetadata_JsonSerializationException_ReturnsEmptyMetadata( + Guid organizationId, + Guid userId) + { + // Arrange + var currentContext = Substitute.For(); + currentContext.UserId.Returns(userId); + currentContext.OrganizationOwner(organizationId).Returns(true); + + var errorDictionary = BuildErrorDictionary(); + + var model = new SavePolicyRequest + { + Policy = new PolicyRequestModel + { + Type = PolicyType.OrganizationDataOwnership, + Enabled = true, + Data = null + }, + Metadata = errorDictionary + }; + + // Act + var result = await model.ToSavePolicyModelAsync(organizationId, currentContext); + + // Assert + Assert.NotNull(result); + Assert.IsType(result.Metadata); + } + + private static Dictionary BuildErrorDictionary() + { + var circularDict = new Dictionary(); + circularDict["self"] = circularDict; + return circularDict; + } +} diff --git a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs index 794f6fddf3..4d00476645 100644 --- a/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs +++ b/test/Core.Test/AdminConsole/AutoFixture/PolicyUpdateFixtures.cs @@ -18,7 +18,7 @@ internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICusto } } -public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute +public class PolicyUpdateAttribute(PolicyType type = PolicyType.FreeFamiliesSponsorshipPolicy, bool enabled = true) : CustomizeAttribute { public override ICustomization GetCustomization(ParameterInfo parameter) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs index 2569bc6988..a39382382b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationDataOwnershipPolicyValidatorTests.cs @@ -10,7 +10,6 @@ using Bit.Core.Services; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -22,9 +21,10 @@ public class OrganizationDataOwnershipPolicyValidatorTests private const string _defaultUserCollectionName = "Default"; [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_FeatureFlagDisabled_DoesNothing( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_FeatureFlagDisabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange @@ -32,95 +32,102 @@ public class OrganizationDataOwnershipPolicyValidatorTests .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(false); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); + // Act - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_PolicyAlreadyEnabled_DoesNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; - policyUpdate.Enabled = true; + 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.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_PolicyBeingDisabled_DoesNothing( + public async Task ExecuteSideEffectsAsync_PolicyBeingDisabled_DoesNothing( [PolicyUpdate(PolicyType.OrganizationDataOwnership, false)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, true)] Policy currentPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership)] Policy previousPolicyState, SutProvider sutProvider) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; + 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.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sutProvider.Sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await sutProvider.GetDependency() .DidNotReceive() - .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenNoUsersExist_ShouldLogError( - [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy, + public async Task ExecuteSideEffectsAsync_WhenNoUsersExist_DoNothing( + [PolicyUpdate(PolicyType.OrganizationDataOwnership, true)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, true)] Policy postUpdatedPolicy, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy previousPolicyState, OrganizationDataOwnershipPolicyRequirementFactory factory) { // Arrange - currentPolicy.OrganizationId = policyUpdate.OrganizationId; - policyUpdate.Enabled = true; + postUpdatedPolicy.OrganizationId = policyUpdate.OrganizationId; + previousPolicyState.OrganizationId = policyUpdate.OrganizationId; - var policyRepository = ArrangePolicyRepositoryWithOutUsers(); + var policyRepository = ArrangePolicyRepository([]); var collectionRepository = Substitute.For(); - var logger = Substitute.For>(); - var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); // Act - await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await collectionRepository .DidNotReceive() .UpsertDefaultCollectionsAsync( Arg.Any(), - Arg.Any>(), + Arg.Any>(), Arg.Any()); - const string expectedErrorMessage = "No UserOrganizationIds found for"; - - logger.Received(1).Log( - LogLevel.Error, - Arg.Any(), - Arg.Is(o => (o.ToString() ?? "").Contains(expectedErrorMessage)), - Arg.Any(), - Arg.Any>()); + await policyRepository + .Received(1) + .GetPolicyDetailsByOrganizationIdAsync( + policyUpdate.OrganizationId, + PolicyType.OrganizationDataOwnership); } public static IEnumerable ShouldUpsertDefaultCollectionsTestCases() @@ -133,13 +140,13 @@ public class OrganizationDataOwnershipPolicyValidatorTests object?[] WithExistingPolicy() { var organizationId = Guid.NewGuid(); - var policyUpdate = new PolicyUpdate + var postUpdatedPolicy = new Policy { OrganizationId = organizationId, Type = PolicyType.OrganizationDataOwnership, Enabled = true }; - var currentPolicy = new Policy + var previousPolicyState = new Policy { Id = Guid.NewGuid(), OrganizationId = organizationId, @@ -149,51 +156,53 @@ public class OrganizationDataOwnershipPolicyValidatorTests return new object?[] { - policyUpdate, - currentPolicy + postUpdatedPolicy, + previousPolicyState }; } object?[] WithNoExistingPolicy() { - var policyUpdate = new PolicyUpdate + var postUpdatedPolicy = new Policy { OrganizationId = new Guid(), Type = PolicyType.OrganizationDataOwnership, Enabled = true }; - const Policy currentPolicy = null; + const Policy previousPolicyState = null; return new object?[] { - policyUpdate, - currentPolicy + postUpdatedPolicy, + previousPolicyState }; } } - [Theory, BitAutoData] + [Theory] [BitMemberAutoData(nameof(ShouldUpsertDefaultCollectionsTestCases))] - public async Task OnSaveSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( + public async Task ExecuteSideEffectsAsync_WithRequirements_ShouldUpsertDefaultCollections( + Policy postUpdatedPolicy, + Policy? previousPolicyState, [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, - [Policy(PolicyType.OrganizationDataOwnership, false)] Policy? currentPolicy, [OrganizationPolicyDetails(PolicyType.OrganizationDataOwnership)] IEnumerable orgPolicyDetails, OrganizationDataOwnershipPolicyRequirementFactory factory) { // Arrange - foreach (var policyDetail in orgPolicyDetails) + var orgPolicyDetailsList = orgPolicyDetails.ToList(); + foreach (var policyDetail in orgPolicyDetailsList) { policyDetail.OrganizationId = policyUpdate.OrganizationId; } - var policyRepository = ArrangePolicyRepository(orgPolicyDetails); + var policyRepository = ArrangePolicyRepository(orgPolicyDetailsList); var collectionRepository = Substitute.For(); - var logger = Substitute.For>(); - var sut = ArrangeSut(factory, policyRepository, collectionRepository, logger); + var sut = ArrangeSut(factory, policyRepository, collectionRepository); + var policyRequest = new SavePolicyModel(policyUpdate, null, new OrganizationModelOwnershipPolicyModel(_defaultUserCollectionName)); // Act - await sut.OnSaveSideEffectsAsync(policyUpdate, currentPolicy); + await sut.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); // Assert await collectionRepository @@ -204,9 +213,40 @@ public class OrganizationDataOwnershipPolicyValidatorTests _defaultUserCollectionName); } - private static IPolicyRepository ArrangePolicyRepositoryWithOutUsers() + private static IEnumerable WhenDefaultCollectionsDoesNotExistTestCases() { - return ArrangePolicyRepository([]); + yield return [new OrganizationModelOwnershipPolicyModel(null)]; + yield return [new OrganizationModelOwnershipPolicyModel("")]; + yield return [new OrganizationModelOwnershipPolicyModel(" ")]; + yield return [new EmptyMetadataModel()]; + } + [Theory] + [BitMemberAutoData(nameof(WhenDefaultCollectionsDoesNotExistTestCases))] + public async Task ExecuteSideEffectsAsync_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.ExecuteSideEffectsAsync(policyRequest, postUpdatedPolicy, previousPolicyState); + + // Assert + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertDefaultCollectionsAsync(Arg.Any(), Arg.Any>(), Arg.Any()); } private static IPolicyRepository ArrangePolicyRepository(IEnumerable policyDetails) @@ -222,17 +262,15 @@ public class OrganizationDataOwnershipPolicyValidatorTests private static OrganizationDataOwnershipPolicyValidator ArrangeSut( OrganizationDataOwnershipPolicyRequirementFactory factory, IPolicyRepository policyRepository, - ICollectionRepository collectionRepository, - ILogger logger = null!) + ICollectionRepository collectionRepository) { - logger ??= Substitute.For>(); var featureService = Substitute.For(); featureService .IsEnabled(FeatureFlagKeys.CreateDefaultLocation) .Returns(true); - var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService, logger); + var sut = new OrganizationDataOwnershipPolicyValidator(policyRepository, collectionRepository, [factory], featureService); return sut; } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs index aec1230423..bda927f184 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/OrganizationPolicyValidatorTests.cs @@ -1,7 +1,5 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; +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.PolicyRequirements; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.Repositories; @@ -161,20 +159,6 @@ public class TestOrganizationPolicyValidator : OrganizationPolicyValidator { } - public override PolicyType Type => PolicyType.TwoFactorAuthentication; - - public override IEnumerable RequiredPolicies => []; - - public override Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) - { - return Task.FromResult(""); - } - - public override Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) - { - return Task.CompletedTask; - } - public async Task> TestGetUserPolicyRequirementsByOrganizationIdAsync(Guid organizationId, PolicyType policyType) where T : IPolicyRequirement { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs index 426389f33c..6b85760794 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/SavePolicyCommandTests.cs @@ -94,8 +94,8 @@ public class SavePolicyCommandTests Substitute.For(), Substitute.For(), [new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()], - Substitute.For() - )); + Substitute.For(), + Substitute.For())); Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message); } @@ -281,6 +281,85 @@ public class SavePolicyCommandTests await AssertPolicyNotSavedAsync(sutProvider); } + [Theory, BitAutoData] + public async Task VNextSaveAsync_OrganizationDataOwnershipPolicy_ExecutesPostSaveSideEffects( + [PolicyUpdate(PolicyType.OrganizationDataOwnership)] PolicyUpdate policyUpdate, + [Policy(PolicyType.OrganizationDataOwnership, false)] Policy currentPolicy) + { + // Arrange + var sutProvider = SutProviderFactory(); + 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 + var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(result); + + await sutProvider.GetDependency() + .Received(1) + .LogPolicyEventAsync(result, EventType.Policy_Updated); + + await sutProvider.GetDependency() + .Received(1) + .ExecuteSideEffectsAsync(savePolicyModel, result, currentPolicy); + } + + [Theory] + [BitAutoData(PolicyType.SingleOrg)] + [BitAutoData(PolicyType.TwoFactorAuthentication)] + public async Task VNextSaveAsync_NonOrganizationDataOwnershipPolicy_DoesNotExecutePostSaveSideEffects( + PolicyType policyType, + Policy currentPolicy, + [PolicyUpdate] PolicyUpdate policyUpdate) + { + // Arrange + policyUpdate.Type = policyType; + currentPolicy.Type = policyType; + currentPolicy.OrganizationId = policyUpdate.OrganizationId; + + + var sutProvider = SutProviderFactory(); + var savePolicyModel = new SavePolicyModel(policyUpdate, null, new EmptyMetadataModel()); + + sutProvider.GetDependency() + .GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type) + .Returns(currentPolicy); + + ArrangeOrganization(sutProvider, policyUpdate); + sutProvider.GetDependency() + .GetManyByOrganizationIdAsync(policyUpdate.OrganizationId) + .Returns([currentPolicy]); + + // Act + var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertAsync(result); + + await sutProvider.GetDependency() + .Received(1) + .LogPolicyEventAsync(result, EventType.Policy_Updated); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ExecuteSideEffectsAsync(default!, default!, default!); + } + /// /// Returns a new SutProvider with the PolicyValidators registered in the Sut. /// @@ -289,6 +368,7 @@ public class SavePolicyCommandTests return new SutProvider() .WithFakeTimeProvider() .SetDependency(policyValidators ?? []) + .SetDependency(Substitute.For()) .Create(); } From a458db319ea579f412b4760d18bf2c922bee2a86 Mon Sep 17 00:00:00 2001 From: Kyle Denney <4227399+kdenney@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:08:22 -0500 Subject: [PATCH 223/326] [PM-25088] - refactor premium purchase endpoint (#6262) * [PM-25088] add feature flag for new premium subscription flow * [PM-25088] refactor premium endpoint * forgot the punctuation change in the test * [PM-25088] - pr feedback * [PM-25088] - pr feedback round two --- .../PaymentMethodTypeValidationAttribute.cs | 13 + .../VNext/AccountBillingVNextController.cs | 17 + .../SelfHostedAccountBillingController.cs | 38 ++ .../MinimalTokenizedPaymentMethodRequest.cs | 25 + .../Payment/TokenizedPaymentMethodRequest.cs | 14 +- .../PremiumCloudHostedSubscriptionRequest.cs | 26 + .../PremiumSelfHostedSubscriptionRequest.cs | 10 + .../Services/OrganizationFactory.cs | 4 +- src/Core/Billing/Constants/StripeConstants.cs | 1 + .../Extensions/ServiceCollectionExtensions.cs | 8 + .../Models/TokenizablePaymentMethodType.cs | 14 + ...tePremiumCloudHostedSubscriptionCommand.cs | 308 +++++++++++ ...atePremiumSelfHostedSubscriptionCommand.cs | 67 +++ .../Billing/Services/ILicensingService.cs | 1 + .../Implementations/LicensingService.cs | 8 + .../PremiumUserBillingService.cs | 2 +- .../NoopLicensingService.cs | 5 + src/Core/Constants.cs | 6 + .../Implementations/StripePaymentService.cs | 2 +- .../Services/Implementations/UserService.cs | 6 +- .../Services/SendValidationService.cs | 2 +- .../Services/Implementations/CipherService.cs | 2 +- ...miumCloudHostedSubscriptionCommandTests.cs | 477 ++++++++++++++++++ ...emiumSelfHostedSubscriptionCommandTests.cs | 199 ++++++++ .../Billing/Services/LicensingServiceTests.cs | 75 +++ 25 files changed, 1309 insertions(+), 21 deletions(-) create mode 100644 src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs create mode 100644 src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs create mode 100644 src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Premium/PremiumSelfHostedSubscriptionRequest.cs create mode 100644 src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs create mode 100644 src/Core/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommand.cs create mode 100644 test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs create mode 100644 test/Core.Test/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommandTests.cs diff --git a/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs b/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs new file mode 100644 index 0000000000..227b454f9f --- /dev/null +++ b/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs @@ -0,0 +1,13 @@ +using Bit.Api.Utilities; + +namespace Bit.Api.Billing.Attributes; + +public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute +{ + private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"]; + + public PaymentMethodTypeValidationAttribute() : base(_acceptedValues) + { + ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}"; + } +} diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs index e3b702e36d..a996290507 100644 --- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -1,8 +1,11 @@ #nullable enable using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Api.Billing.Models.Requests.Premium; +using Bit.Core; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Premium.Commands; using Bit.Core.Entities; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -16,6 +19,7 @@ namespace Bit.Api.Billing.Controllers.VNext; [SelfHosted(NotSelfHostedOnly = true)] public class AccountBillingVNextController( ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand, + ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand, IGetCreditQuery getCreditQuery, IGetPaymentMethodQuery getPaymentMethodQuery, IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController @@ -61,4 +65,17 @@ public class AccountBillingVNextController( var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress); return Handle(result); } + + [HttpPost("subscription")] + [RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)] + [InjectUser] + public async Task CreateSubscriptionAsync( + [BindNever] User user, + [FromBody] PremiumCloudHostedSubscriptionRequest request) + { + var (paymentMethod, billingAddress, additionalStorageGb) = request.ToDomain(); + var result = await createPremiumCloudHostedSubscriptionCommand.Run( + user, paymentMethod, billingAddress, additionalStorageGb); + return Handle(result); + } } diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs new file mode 100644 index 0000000000..544753ad0f --- /dev/null +++ b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs @@ -0,0 +1,38 @@ +#nullable enable +using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Models.Requests.Premium; +using Bit.Api.Utilities; +using Bit.Core; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Bit.Api.Billing.Controllers.VNext; + +[Authorize("Application")] +[Route("account/billing/vnext/self-host")] +[SelfHosted(SelfHostedOnly = true)] +public class SelfHostedAccountBillingController( + ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController +{ + [HttpPost("license")] + [RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)] + [InjectUser] + public async Task UploadLicenseAsync( + [BindNever] User user, + PremiumSelfHostedSubscriptionRequest request) + { + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, request.License); + if (license == null) + { + throw new BadRequestException("Invalid license."); + } + var result = await createPremiumSelfHostedSubscriptionCommand.Run(user, license); + return Handle(result); + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs new file mode 100644 index 0000000000..3b50d2bf63 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs @@ -0,0 +1,25 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Attributes; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Payment; + +public class MinimalTokenizedPaymentMethodRequest +{ + [Required] + [PaymentMethodTypeValidation] + public required string Type { get; set; } + + [Required] + public required string Token { get; set; } + + public TokenizedPaymentMethod ToDomain() + { + return new TokenizedPaymentMethod + { + Type = TokenizablePaymentMethodTypeExtensions.From(Type), + Token = Token + }; + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs index 663e4e7cd2..f540957a1a 100644 --- a/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs @@ -1,6 +1,6 @@ #nullable enable using System.ComponentModel.DataAnnotations; -using Bit.Api.Utilities; +using Bit.Api.Billing.Attributes; using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Payment; @@ -8,8 +8,7 @@ namespace Bit.Api.Billing.Models.Requests.Payment; public class TokenizedPaymentMethodRequest { [Required] - [StringMatches("bankAccount", "card", "payPal", - ErrorMessage = "Payment method type must be one of: bankAccount, card, payPal")] + [PaymentMethodTypeValidation] public required string Type { get; set; } [Required] @@ -21,14 +20,7 @@ public class TokenizedPaymentMethodRequest { var paymentMethod = new TokenizedPaymentMethod { - Type = Type switch - { - "bankAccount" => TokenizablePaymentMethodType.BankAccount, - "card" => TokenizablePaymentMethodType.Card, - "payPal" => TokenizablePaymentMethodType.PayPal, - _ => throw new InvalidOperationException( - $"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}") - }, + Type = TokenizablePaymentMethodTypeExtensions.From(Type), Token = Token }; diff --git a/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs new file mode 100644 index 0000000000..b958057f5b --- /dev/null +++ b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs @@ -0,0 +1,26 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Premium; + +public class PremiumCloudHostedSubscriptionRequest +{ + [Required] + public required MinimalTokenizedPaymentMethodRequest TokenizedPaymentMethod { get; set; } + + [Required] + public required MinimalBillingAddressRequest BillingAddress { get; set; } + + [Range(0, 99)] + public short AdditionalStorageGb { get; set; } = 0; + + public (TokenizedPaymentMethod, BillingAddress, short) ToDomain() + { + var paymentMethod = TokenizedPaymentMethod.ToDomain(); + var billingAddress = BillingAddress.ToDomain(); + + return (paymentMethod, billingAddress, AdditionalStorageGb); + } +} diff --git a/src/Api/Billing/Models/Requests/Premium/PremiumSelfHostedSubscriptionRequest.cs b/src/Api/Billing/Models/Requests/Premium/PremiumSelfHostedSubscriptionRequest.cs new file mode 100644 index 0000000000..261544476e --- /dev/null +++ b/src/Api/Billing/Models/Requests/Premium/PremiumSelfHostedSubscriptionRequest.cs @@ -0,0 +1,10 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Billing.Models.Requests.Premium; + +public class PremiumSelfHostedSubscriptionRequest +{ + [Required] + public required IFormFile License { get; set; } +} diff --git a/src/Core/AdminConsole/Services/OrganizationFactory.cs b/src/Core/AdminConsole/Services/OrganizationFactory.cs index dbc8f0fa21..afb3931ec4 100644 --- a/src/Core/AdminConsole/Services/OrganizationFactory.cs +++ b/src/Core/AdminConsole/Services/OrganizationFactory.cs @@ -23,7 +23,7 @@ public static class OrganizationFactory PlanType = claimsPrincipal.GetValue(OrganizationLicenseConstants.PlanType), Seats = claimsPrincipal.GetValue(OrganizationLicenseConstants.Seats), MaxCollections = claimsPrincipal.GetValue(OrganizationLicenseConstants.MaxCollections), - MaxStorageGb = 10240, + MaxStorageGb = Constants.SelfHostedMaxStorageGb, UsePolicies = claimsPrincipal.GetValue(OrganizationLicenseConstants.UsePolicies), UseSso = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseSso), UseKeyConnector = claimsPrincipal.GetValue(OrganizationLicenseConstants.UseKeyConnector), @@ -75,7 +75,7 @@ public static class OrganizationFactory PlanType = license.PlanType, Seats = license.Seats, MaxCollections = license.MaxCollections, - MaxStorageGb = 10240, + MaxStorageGb = Constants.SelfHostedMaxStorageGb, UsePolicies = license.UsePolicies, UseSso = license.UseSso, UseKeyConnector = license.UseKeyConnector, diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 2be88902c8..131adfedf8 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -79,6 +79,7 @@ public static class StripeConstants public static class Prices { public const string StoragePlanPersonal = "personal-storage-gb-annually"; + public const string PremiumAnnually = "premium-annually"; } public static class ProrationBehavior diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 147e96105a..b4e37f0151 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Bit.Core.Billing.Organizations.Commands; using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Billing.Payment; +using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; @@ -30,6 +31,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); + services.AddPremiumCommands(); services.AddTransient(); } @@ -39,4 +41,10 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); } + + private static void AddPremiumCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } } diff --git a/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs b/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs index d27a924360..c198ec8230 100644 --- a/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs +++ b/src/Core/Billing/Payment/Models/TokenizablePaymentMethodType.cs @@ -6,3 +6,17 @@ public enum TokenizablePaymentMethodType Card, PayPal } + +public static class TokenizablePaymentMethodTypeExtensions +{ + public static TokenizablePaymentMethodType From(string type) + { + return type switch + { + "bankAccount" => TokenizablePaymentMethodType.BankAccount, + "card" => TokenizablePaymentMethodType.Card, + "payPal" => TokenizablePaymentMethodType.PayPal, + _ => throw new InvalidOperationException($"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}") + }; + } +} diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs new file mode 100644 index 0000000000..8a73f31880 --- /dev/null +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -0,0 +1,308 @@ +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Braintree; +using Microsoft.Extensions.Logging; +using OneOf.Types; +using Stripe; +using Customer = Stripe.Customer; +using Subscription = Stripe.Subscription; + +namespace Bit.Core.Billing.Premium.Commands; + +using static Utilities; + +/// +/// Creates a premium subscription for a cloud-hosted user with Stripe payment processing. +/// Handles customer creation, payment method setup, and subscription creation. +/// +public interface ICreatePremiumCloudHostedSubscriptionCommand +{ + /// + /// Creates a premium cloud-hosted subscription for the specified user. + /// + /// The user to create the premium subscription for. Must not already be a premium user. + /// The tokenized payment method containing the payment type and token for billing. + /// The billing address information required for tax calculation and customer creation. + /// Additional storage in GB beyond the base 1GB included with premium (must be >= 0). + /// A billing command result indicating success or failure with appropriate error details. + Task> Run( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress, + short additionalStorageGb); +} + +public class CreatePremiumCloudHostedSubscriptionCommand( + IBraintreeGateway braintreeGateway, + IGlobalSettings globalSettings, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService, + IUserService userService, + IPushNotificationService pushNotificationService, + ILogger logger) + : BaseBillingCommand(logger), ICreatePremiumCloudHostedSubscriptionCommand +{ + private static readonly List _expand = ["tax"]; + private readonly ILogger _logger = logger; + + public Task> Run( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress, + short additionalStorageGb) => HandleAsync(async () => + { + if (user.Premium) + { + return new BadRequest("Already a premium user."); + } + + if (additionalStorageGb < 0) + { + return new BadRequest("Additional storage must be greater than 0."); + } + + var customer = string.IsNullOrEmpty(user.GatewayCustomerId) + ? await CreateCustomerAsync(user, paymentMethod, billingAddress) + : await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand }); + + customer = await ReconcileBillingLocationAsync(customer, billingAddress); + + 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: + { + user.Premium = true; + user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + break; + } + } + + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + user.GatewaySubscriptionId = subscription.Id; + user.MaxStorageGb = (short)(1 + additionalStorageGb); + user.LicenseKey = CoreHelpers.SecureRandomString(20); + user.RevisionDate = DateTime.UtcNow; + + await userService.SaveUserAsync(user); + await pushNotificationService.PushSyncVaultAsync(user.Id); + + return new None(); + }); + + private async Task CreateCustomerAsync(User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + var subscriberName = user.SubscriberName(); + var customerCreateOptions = new CustomerCreateOptions + { + Address = new AddressOptions + { + Line1 = billingAddress.Line1, + Line2 = billingAddress.Line2, + City = billingAddress.City, + PostalCode = billingAddress.PostalCode, + State = billingAddress.State, + Country = billingAddress.Country + }, + Description = user.Name, + Email = user.Email, + Expand = _expand, + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = user.SubscriberType(), + Value = subscriberName.Length <= 30 + ? subscriberName + : subscriberName[..30] + } + ] + }, + Metadata = new Dictionary + { + [StripeConstants.MetadataKeys.Region] = globalSettings.BaseServiceUri.CloudRegion, + [StripeConstants.MetadataKeys.UserId] = user.Id.ToString() + }, + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + } + }; + + var braintreeCustomerId = ""; + + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch (paymentMethod.Type) + { + case TokenizablePaymentMethodType.BankAccount: + { + var setupIntent = + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.Token })) + .FirstOrDefault(); + + if (setupIntent == null) + { + _logger.LogError("Cannot create customer for user ({UserID}) without a setup intent for their bank account", user.Id); + throw new BillingException(); + } + + await setupIntentCache.Set(user.Id, setupIntent.Id); + break; + } + case TokenizablePaymentMethodType.Card: + { + customerCreateOptions.PaymentMethod = paymentMethod.Token; + customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token; + break; + } + case TokenizablePaymentMethodType.PayPal: + { + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.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()); + throw new BillingException(); + } + } + + try + { + return await stripeAdapter.CustomerCreateAsync(customerCreateOptions); + } + catch + { + await Revert(); + throw; + } + + async Task Revert() + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (paymentMethod.Type) + { + case TokenizablePaymentMethodType.BankAccount: + { + await setupIntentCache.Remove(user.Id); + break; + } + case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } + } + } + } + + private async Task ReconcileBillingLocationAsync( + Customer customer, + BillingAddress billingAddress) + { + /* + * If the customer was previously set up with credit, which does not require a billing location, + * we need to update the customer on the fly before we start the subscription. + */ + if (customer is { Address: { Country: not null and not "", PostalCode: not null and not "" } }) + { + return customer; + } + + var options = new CustomerUpdateOptions + { + Address = new AddressOptions + { + Line1 = billingAddress.Line1, + Line2 = billingAddress.Line2, + City = billingAddress.City, + PostalCode = billingAddress.PostalCode, + State = billingAddress.State, + Country = billingAddress.Country + }, + Expand = _expand, + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + } + }; + return await stripeAdapter.CustomerUpdateAsync(customer.Id, options); + } + + private async Task CreateSubscriptionAsync( + Guid userId, + Customer customer, + int? storage) + { + var subscriptionItemOptionsList = new List + { + new () + { + Price = StripeConstants.Prices.PremiumAnnually, + Quantity = 1 + } + }; + + if (storage is > 0) + { + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Price = StripeConstants.Prices.StoragePlanPersonal, + Quantity = storage + }); + } + + var usingPayPal = customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) ?? false; + + var subscriptionCreateOptions = new SubscriptionCreateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }, + CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, + Customer = customer.Id, + Items = subscriptionItemOptionsList, + Metadata = new Dictionary + { + [StripeConstants.MetadataKeys.UserId] = userId.ToString() + }, + PaymentBehavior = usingPayPal + ? StripeConstants.PaymentBehavior.DefaultIncomplete + : null, + OffSession = true + }; + + var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); + + if (usingPayPal) + { + await stripeAdapter.InvoiceUpdateAsync(subscription.LatestInvoiceId, new InvoiceUpdateOptions + { + AutoAdvance = false + }); + } + + return subscription; + } +} diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommand.cs new file mode 100644 index 0000000000..7546149ab6 --- /dev/null +++ b/src/Core/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommand.cs @@ -0,0 +1,67 @@ +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using OneOf.Types; + +namespace Bit.Core.Billing.Premium.Commands; + +/// +/// Creates a premium subscription for a self-hosted user. +/// Validates the license and applies premium benefits including storage limits based on the license terms. +/// +public interface ICreatePremiumSelfHostedSubscriptionCommand +{ + /// + /// Creates a premium self-hosted subscription for the specified user using the provided license. + /// + /// The user to create the premium subscription for. Must not already be a premium user. + /// The user license containing the premium subscription details and verification data. Must be valid and usable by the specified user. + /// A billing command result indicating success or failure with appropriate error details. + Task> Run(User user, UserLicense license); +} + +public class CreatePremiumSelfHostedSubscriptionCommand( + ILicensingService licensingService, + IUserService userService, + IPushNotificationService pushNotificationService, + ILogger logger) + : BaseBillingCommand(logger), ICreatePremiumSelfHostedSubscriptionCommand +{ + public Task> Run( + User user, + UserLicense license) => HandleAsync(async () => + { + if (user.Premium) + { + return new BadRequest("Already a premium user."); + } + + if (!licensingService.VerifyLicense(license)) + { + return new BadRequest("Invalid license."); + } + + var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license); + if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage)) + { + return new BadRequest(exceptionMessage); + } + + await licensingService.WriteUserLicenseAsync(user, license); + + user.Premium = true; + user.RevisionDate = DateTime.UtcNow; + user.MaxStorageGb = Core.Constants.SelfHostedMaxStorageGb; + user.LicenseKey = license.LicenseKey; + user.PremiumExpirationDate = license.Expires; + + await userService.SaveUserAsync(user); + await pushNotificationService.PushSyncVaultAsync(user.Id); + + return new None(); + }); +} diff --git a/src/Core/Billing/Services/ILicensingService.cs b/src/Core/Billing/Services/ILicensingService.cs index b6ada998a7..cd9847ea39 100644 --- a/src/Core/Billing/Services/ILicensingService.cs +++ b/src/Core/Billing/Services/ILicensingService.cs @@ -26,4 +26,5 @@ public interface ILicensingService SubscriptionInfo subscriptionInfo); Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo); + Task WriteUserLicenseAsync(User user, UserLicense license); } diff --git a/src/Core/Billing/Services/Implementations/LicensingService.cs b/src/Core/Billing/Services/Implementations/LicensingService.cs index 81a52158ce..6f0cdec8f5 100644 --- a/src/Core/Billing/Services/Implementations/LicensingService.cs +++ b/src/Core/Billing/Services/Implementations/LicensingService.cs @@ -389,4 +389,12 @@ public class LicensingService : ILicensingService var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } + + public async Task WriteUserLicenseAsync(User user, UserLicense license) + { + var dir = $"{_globalSettings.LicenseDirectory}/user"; + Directory.CreateDirectory(dir); + await using var fs = File.OpenWrite(Path.Combine(dir, $"{user.Id}.json")); + await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented); + } } diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 986991ba0a..9db18278b6 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -304,7 +304,7 @@ public class PremiumUserBillingService( { new () { - Price = "premium-annually", + Price = StripeConstants.Prices.PremiumAnnually, Quantity = 1 } }; diff --git a/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs index a54ba3546a..b27e21a7c9 100644 --- a/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Billing/Services/NoopImplementations/NoopLicensingService.cs @@ -73,4 +73,9 @@ public class NoopLicensingService : ILicensingService { return Task.FromResult(null); } + + public Task WriteUserLicenseAsync(User user, UserLicense license) + { + return Task.CompletedTask; + } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 69003ee253..cba060427c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -10,6 +10,11 @@ public static class Constants public const int BypassFiltersEventId = 12482444; public const int FailedSecretVerificationDelay = 2000; + /// + /// Self-hosted max storage limit in GB (10 TB). + /// + public const short SelfHostedMaxStorageGb = 10240; + // File size limits - give 1 MB extra for cushion. // Note: if request size limits are changed, 'client_max_body_size' // in nginx/proxy.conf may also need to be updated accordingly. @@ -166,6 +171,7 @@ public static class FeatureFlagKeys public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; + public const string PM23385_UseNewPremiumFlow = "pm-23385-use-new-premium-flow"; /* 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 ec45944bd2..5b68906d8a 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -906,7 +906,7 @@ public class StripePaymentService : IPaymentService new() { Quantity = 1, - Plan = "premium-annually" + Plan = StripeConstants.Prices.PremiumAnnually }, new() diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 16e298d177..386cb8c3d2 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -44,8 +44,6 @@ namespace Bit.Core.Services; public class UserService : UserManager, IUserService { - private const string PremiumPlanId = "premium-annually"; - private readonly IUserRepository _userRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; @@ -930,7 +928,7 @@ public class UserService : UserManager, IUserService if (_globalSettings.SelfHosted) { - user.MaxStorageGb = 10240; // 10 TB + user.MaxStorageGb = Constants.SelfHostedMaxStorageGb; user.LicenseKey = license.LicenseKey; user.PremiumExpirationDate = license.Expires; } @@ -989,7 +987,7 @@ public class UserService : UserManager, IUserService user.Premium = license.Premium; user.RevisionDate = DateTime.UtcNow; - user.MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb; // 10 TB + user.MaxStorageGb = _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : license.MaxStorageGb; user.LicenseKey = license.LicenseKey; user.PremiumExpirationDate = license.Expires; await SaveUserAsync(user); diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs index c6dd3b1dc9..c545c8b35f 100644 --- a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -125,7 +125,7 @@ public class SendValidationService : ISendValidationService { // Users that get access to file storage/premium from their organization get the default // 1 GB max storage. - short limit = _globalSettings.SelfHosted ? (short)10240 : (short)1; + short limit = _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1; storageBytesRemaining = user.StorageBytesRemaining(limit); } } diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 2a4cc6c137..e0b121fdd3 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -933,7 +933,7 @@ public class CipherService : ICipherService // Users that get access to file storage/premium from their organization get the default // 1 GB max storage. storageBytesRemaining = user.StorageBytesRemaining( - _globalSettings.SelfHosted ? (short)10240 : (short)1); + _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1); } } else if (cipher.OrganizationId.HasValue) diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs new file mode 100644 index 0000000000..e808fb10b0 --- /dev/null +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommandTests.cs @@ -0,0 +1,477 @@ +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture.Attributes; +using Braintree; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; +using Address = Stripe.Address; +using StripeCustomer = Stripe.Customer; +using StripeSubscription = Stripe.Subscription; + +namespace Bit.Core.Test.Billing.Premium.Commands; + +public class CreatePremiumCloudHostedSubscriptionCommandTests +{ + private readonly IBraintreeGateway _braintreeGateway = Substitute.For(); + private readonly IGlobalSettings _globalSettings = Substitute.For(); + private readonly ISetupIntentCache _setupIntentCache = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly IUserService _userService = Substitute.For(); + private readonly IPushNotificationService _pushNotificationService = Substitute.For(); + private readonly CreatePremiumCloudHostedSubscriptionCommand _command; + + public CreatePremiumCloudHostedSubscriptionCommandTests() + { + var baseServiceUri = Substitute.For(); + baseServiceUri.CloudRegion.Returns("US"); + _globalSettings.BaseServiceUri.Returns(baseServiceUri); + + _command = new CreatePremiumCloudHostedSubscriptionCommand( + _braintreeGateway, + _globalSettings, + _setupIntentCache, + _stripeAdapter, + _subscriberService, + _userService, + _pushNotificationService, + Substitute.For>()); + } + + [Theory, BitAutoData] + public async Task Run_UserAlreadyPremium_ReturnsBadRequest( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = true; + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Already a premium user.", badRequest.Response); + } + + [Theory, BitAutoData] + public async Task Run_NegativeStorageAmount_ReturnsBadRequest( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, -1); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Additional storage must be greater than 0.", badRequest.Response); + } + + [Theory, BitAutoData] + public async Task Run_ValidPaymentMethodTypes_BankAccount_Success( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; // Ensure no existing customer ID + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.BankAccount; + paymentMethod.Token = "bank_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "active"; + + var mockInvoice = Substitute.For(); + + var mockSetupIntent = Substitute.For(); + mockSetupIntent.Id = "seti_123"; + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _stripeAdapter.SetupIntentList(Arg.Any()).Returns(Task.FromResult(new List { mockSetupIntent })); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); + await _userService.Received(1).SaveUserAsync(user); + await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task Run_ValidPaymentMethodTypes_Card_Success( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.Card; + paymentMethod.Token = "card_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "active"; + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); + await _userService.Received(1).SaveUserAsync(user); + await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task Run_ValidPaymentMethodTypes_PayPal_Success( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.PayPal; + paymentMethod.Token = "paypal_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "active"; + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + await _stripeAdapter.Received(1).CustomerCreateAsync(Arg.Any()); + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); + await _subscriberService.Received(1).CreateBraintreeCustomer(user, paymentMethod.Token); + await _userService.Received(1).SaveUserAsync(user); + await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task Run_ValidRequestWithAdditionalStorage_Success( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.Card; + paymentMethod.Token = "card_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + const short additionalStorage = 2; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_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.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage); + + // Assert + Assert.True(result.IsT0); + Assert.True(user.Premium); + Assert.Equal((short)(1 + additionalStorage), user.MaxStorageGb); + Assert.NotNull(user.LicenseKey); + Assert.Equal(20, user.LicenseKey.Length); + Assert.NotEqual(default, user.RevisionDate); + await _userService.Received(1).SaveUserAsync(user); + await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); + } + + [Theory, BitAutoData] + public async Task Run_UserHasExistingGatewayCustomerId_UsesExistingCustomer( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = "existing_customer_123"; + paymentMethod.Type = TokenizablePaymentMethodType.Card; + 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"; + + 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()); + } + + [Theory, BitAutoData] + public async Task Run_PayPalWithIncompleteSubscription_SetsPremiumTrue( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + user.PremiumExpirationDate = null; + paymentMethod.Type = TokenizablePaymentMethodType.PayPal; + paymentMethod.Token = "paypal_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "incomplete"; + mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + Assert.True(user.Premium); + Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate); + } + + [Theory, BitAutoData] + public async Task Run_NonPayPalWithActiveSubscription_SetsPremiumTrue( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.Card; + paymentMethod.Token = "card_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_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.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + Assert.True(user.Premium); + Assert.Equal(mockSubscription.CurrentPeriodEnd, user.PremiumExpirationDate); + } + + [Theory, BitAutoData] + public async Task Run_SubscriptionStatusDoesNotMatchPatterns_DoesNotSetPremium( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + user.PremiumExpirationDate = null; + paymentMethod.Type = TokenizablePaymentMethodType.PayPal; + paymentMethod.Token = "paypal_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "active"; // PayPal + active doesn't match pattern + mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.CreateBraintreeCustomer(Arg.Any(), Arg.Any()).Returns("bt_customer_123"); + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT0); + Assert.False(user.Premium); + Assert.Null(user.PremiumExpirationDate); + } + + [Theory, BitAutoData] + public async Task Run_BankAccountWithNoSetupIntentFound_ReturnsUnhandled( + User user, + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) + { + // Arrange + user.Premium = false; + user.GatewayCustomerId = null; + user.Email = "test@example.com"; + paymentMethod.Type = TokenizablePaymentMethodType.BankAccount; + paymentMethod.Token = "bank_token_123"; + billingAddress.Country = "US"; + billingAddress.PostalCode = "12345"; + + var mockCustomer = Substitute.For(); + mockCustomer.Id = "cust_123"; + mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" }; + mockCustomer.Metadata = new Dictionary(); + + var mockSubscription = Substitute.For(); + mockSubscription.Id = "sub_123"; + mockSubscription.Status = "incomplete"; + mockSubscription.CurrentPeriodEnd = DateTime.UtcNow.AddDays(30); + + var mockInvoice = Substitute.For(); + + _stripeAdapter.CustomerCreateAsync(Arg.Any()).Returns(mockCustomer); + _stripeAdapter.CustomerUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockCustomer); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(mockSubscription); + _stripeAdapter.InvoiceUpdateAsync(Arg.Any(), Arg.Any()).Returns(mockInvoice); + _subscriberService.GetCustomerOrThrow(Arg.Any(), Arg.Any()).Returns(mockCustomer); + + _stripeAdapter.SetupIntentList(Arg.Any()) + .Returns(Task.FromResult(new List())); // Empty list - no setup intent found + + // Act + var result = await _command.Run(user, paymentMethod, billingAddress, 0); + + // Assert + Assert.True(result.IsT3); + var unhandled = result.AsT3; + Assert.Equal("Something went wrong with your request. Please contact support for assistance.", unhandled.Response); + } +} diff --git a/test/Core.Test/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommandTests.cs new file mode 100644 index 0000000000..6dfd620e45 --- /dev/null +++ b/test/Core.Test/Billing/Premium/Commands/CreatePremiumSelfHostedSubscriptionCommandTests.cs @@ -0,0 +1,199 @@ +using System.Security.Claims; +using Bit.Core.Billing.Models.Business; +using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Billing.Services; +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Billing.Premium.Commands; + +public class CreatePremiumSelfHostedSubscriptionCommandTests +{ + private readonly ILicensingService _licensingService = Substitute.For(); + private readonly IUserService _userService = Substitute.For(); + private readonly IPushNotificationService _pushNotificationService = Substitute.For(); + private readonly CreatePremiumSelfHostedSubscriptionCommand _command; + + public CreatePremiumSelfHostedSubscriptionCommandTests() + { + _command = new CreatePremiumSelfHostedSubscriptionCommand( + _licensingService, + _userService, + _pushNotificationService, + Substitute.For>()); + } + + [Fact] + public async Task Run_UserAlreadyPremium_ReturnsBadRequest() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Premium = true + }; + + var license = new UserLicense + { + LicenseKey = "test_key", + Expires = DateTime.UtcNow.AddYears(1) + }; + + // Act + var result = await _command.Run(user, license); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Already a premium user.", badRequest.Response); + } + + [Fact] + public async Task Run_InvalidLicense_ReturnsBadRequest() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Premium = false + }; + + var license = new UserLicense + { + LicenseKey = "invalid_key", + Expires = DateTime.UtcNow.AddYears(1) + }; + + _licensingService.VerifyLicense(license).Returns(false); + + // Act + var result = await _command.Run(user, license); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Invalid license.", badRequest.Response); + } + + [Fact] + public async Task Run_LicenseCannotBeUsed_EmailNotVerified_ReturnsBadRequest() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Premium = false, + Email = "test@example.com", + EmailVerified = false + }; + + var license = new UserLicense + { + LicenseKey = "test_key", + Expires = DateTime.UtcNow.AddYears(1), + Token = "valid_token" + }; + + var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("Email", "test@example.com") + })); + + _licensingService.VerifyLicense(license).Returns(true); + _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + + // Act + var result = await _command.Run(user, license); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Contains("The user's email is not verified.", badRequest.Response); + } + + [Fact] + public async Task Run_LicenseCannotBeUsed_EmailMismatch_ReturnsBadRequest() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Premium = false, + Email = "user@example.com", + EmailVerified = true + }; + + var license = new UserLicense + { + LicenseKey = "test_key", + Expires = DateTime.UtcNow.AddYears(1), + Token = "valid_token" + }; + + var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("Email", "license@example.com") + })); + + _licensingService.VerifyLicense(license).Returns(true); + _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + + // Act + var result = await _command.Run(user, license); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Contains("The user's email does not match the license email.", badRequest.Response); + } + + [Fact] + public async Task Run_ValidRequest_Success() + { + // Arrange + var userId = Guid.NewGuid(); + var user = new User + { + Id = userId, + Premium = false, + Email = "test@example.com", + EmailVerified = true + }; + + var license = new UserLicense + { + LicenseKey = "test_key_12345", + Expires = DateTime.UtcNow.AddYears(1), + Token = "valid_token" + }; + + var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("Email", "test@example.com") + })); + + _licensingService.VerifyLicense(license).Returns(true); + _licensingService.GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + + // Act + var result = await _command.Run(user, license); + + // Assert + Assert.True(result.IsT0); + + // Verify user was updated correctly + Assert.True(user.Premium); + Assert.NotNull(user.LicenseKey); + Assert.Equal(license.LicenseKey, user.LicenseKey); + Assert.NotEqual(default, user.RevisionDate); + + // Verify services were called + await _licensingService.Received(1).WriteUserLicenseAsync(user, license); + await _userService.Received(1).SaveUserAsync(user); + await _pushNotificationService.Received(1).PushSyncVaultAsync(user.Id); + } +} diff --git a/test/Core.Test/Billing/Services/LicensingServiceTests.cs b/test/Core.Test/Billing/Services/LicensingServiceTests.cs index f33bda2164..cc160dec71 100644 --- a/test/Core.Test/Billing/Services/LicensingServiceTests.cs +++ b/test/Core.Test/Billing/Services/LicensingServiceTests.cs @@ -1,8 +1,10 @@ using System.Text.Json; using AutoFixture; using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Services; +using Bit.Core.Entities; using Bit.Core.Settings; using Bit.Core.Test.Billing.AutoFixture; using Bit.Test.Common.AutoFixture; @@ -16,6 +18,8 @@ public class LicensingServiceTests { private static string licenseFilePath(Guid orgId) => Path.Combine(OrganizationLicenseDirectory.Value, $"{orgId}.json"); + private static string userLicenseFilePath(Guid userId) => + Path.Combine(UserLicenseDirectory.Value, $"{userId}.json"); private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value); private static Lazy OrganizationLicenseDirectory => new(() => { @@ -26,6 +30,15 @@ public class LicensingServiceTests } return directory; }); + private static Lazy UserLicenseDirectory => new(() => + { + var directory = Path.Combine(Path.GetTempPath(), "user"); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return directory; + }); public static SutProvider GetSutProvider() { @@ -57,4 +70,66 @@ public class LicensingServiceTests Directory.Delete(OrganizationLicenseDirectory.Value, true); } } + + [Theory, BitAutoData] + public async Task WriteUserLicense_CreatesFileWithCorrectContent(User user, UserLicense license) + { + // Arrange + var sutProvider = GetSutProvider(); + var expectedFilePath = userLicenseFilePath(user.Id); + + try + { + // Act + await sutProvider.Sut.WriteUserLicenseAsync(user, license); + + // Assert + Assert.True(File.Exists(expectedFilePath)); + var fileContent = await File.ReadAllTextAsync(expectedFilePath); + var actualLicense = JsonSerializer.Deserialize(fileContent); + + Assert.Equal(license.LicenseKey, actualLicense.LicenseKey); + Assert.Equal(license.Id, actualLicense.Id); + Assert.Equal(license.Expires, actualLicense.Expires); + } + finally + { + // Cleanup + if (Directory.Exists(UserLicenseDirectory.Value)) + { + Directory.Delete(UserLicenseDirectory.Value, true); + } + } + } + + [Theory, BitAutoData] + public async Task WriteUserLicense_CreatesDirectoryIfNotExists(User user, UserLicense license) + { + // Arrange + var sutProvider = GetSutProvider(); + + // Ensure directory doesn't exist + if (Directory.Exists(UserLicenseDirectory.Value)) + { + Directory.Delete(UserLicenseDirectory.Value, true); + } + + try + { + // Act + await sutProvider.Sut.WriteUserLicenseAsync(user, license); + + // Assert + Assert.True(Directory.Exists(UserLicenseDirectory.Value)); + Assert.True(File.Exists(userLicenseFilePath(user.Id))); + } + finally + { + // Cleanup + if (Directory.Exists(UserLicenseDirectory.Value)) + { + Directory.Delete(UserLicenseDirectory.Value, true); + } + } + } } From e57569ad572baeed24366fbb2b9c13eebe9f08b7 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Wed, 10 Sep 2025 14:17:45 -0400 Subject: [PATCH 224/326] Alter Integration Template processing to remove keys when encountering null values (#6309) --- .../IntegrationTemplateContext.cs | 5 +- .../Utilities/IntegrationTemplateProcessor.cs | 14 ++- .../IntegrationTemplateContextTests.cs | 102 ++++++++++++++++++ .../IntegrationTemplateProcessorTests.cs | 15 +-- 4 files changed, 114 insertions(+), 22 deletions(-) create mode 100644 test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index 266c810470..79a30c3a02 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -23,6 +24,8 @@ public class IntegrationTemplateContext(EventMessage eventMessage) public Guid? GroupId => Event.GroupId; public Guid? PolicyId => Event.PolicyId; + public string EventMessage => JsonSerializer.Serialize(Event); + public User? User { get; set; } public string? UserName => User?.Name; public string? UserEmail => User?.Email; diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index dceeea85f4..b561e58a86 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -1,6 +1,5 @@ #nullable enable -using System.Text.Json; using System.Text.RegularExpressions; namespace Bit.Core.AdminConsole.Utilities; @@ -20,15 +19,14 @@ public static partial class IntegrationTemplateProcessor return TokenRegex().Replace(template, match => { var propertyName = match.Groups[1].Value; - if (propertyName == "EventMessage") + var property = type.GetProperty(propertyName); + + if (property == null) { - return JsonSerializer.Serialize(values); - } - else - { - var property = type.GetProperty(propertyName); - return property?.GetValue(values)?.ToString() ?? match.Value; + return match.Value; // Return unknown keys as keys - i.e. #Key# } + + return property?.GetValue(values)?.ToString() ?? ""; }); } diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs new file mode 100644 index 0000000000..930b04121c --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs @@ -0,0 +1,102 @@ +#nullable enable +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.Models.Data.EventIntegrations; + +public class IntegrationTemplateContextTests +{ + [Theory, BitAutoData] + public void EventMessage_ReturnsSerializedJsonOfEvent(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage: eventMessage); + var expected = JsonSerializer.Serialize(eventMessage); + + Assert.Equal(expected, sut.EventMessage); + } + + [Theory, BitAutoData] + public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = user }; + + Assert.Equal(user.Name, sut.UserName); + } + + [Theory, BitAutoData] + public void UserName_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = null }; + + Assert.Null(sut.UserName); + } + + [Theory, BitAutoData] + public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = user }; + + Assert.Equal(user.Email, sut.UserEmail); + } + + [Theory, BitAutoData] + public void UserEmail_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = null }; + + Assert.Null(sut.UserEmail); + } + + [Theory, BitAutoData] + public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; + + Assert.Equal(actingUser.Name, sut.ActingUserName); + } + + [Theory, BitAutoData] + public void ActingUserName_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null }; + + Assert.Null(sut.ActingUserName); + } + + [Theory, BitAutoData] + public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; + + Assert.Equal(actingUser.Email, sut.ActingUserEmail); + } + + [Theory, BitAutoData] + public void ActingUserEmail_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null }; + + Assert.Null(sut.ActingUserEmail); + } + + [Theory, BitAutoData] + public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization) + { + var sut = new IntegrationTemplateContext(eventMessage) { Organization = organization }; + + Assert.Equal(organization.DisplayName(), sut.OrganizationName); + } + + [Theory, BitAutoData] + public void OrganizationName_WhenOrganizationIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { Organization = null }; + + Assert.Null(sut.OrganizationName); + } +} diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index 105b65d0da..d9df9486b6 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -1,6 +1,5 @@ #nullable enable -using System.Text.Json; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Models.Data; using Bit.Test.Common.AutoFixture.Attributes; @@ -41,22 +40,12 @@ public class IntegrationTemplateProcessorTests } [Theory, BitAutoData] - public void ReplaceTokens_WithEventMessageToken_ReplacesWithSerializedJson(EventMessage eventMessage) - { - var template = "#EventMessage#"; - var expected = $"{JsonSerializer.Serialize(eventMessage)}"; - var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage); - - Assert.Equal(expected, result); - } - - [Theory, BitAutoData] - public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage) + public void ReplaceTokens_WithNullProperty_InsertsEmptyString(EventMessage eventMessage) { eventMessage.UserId = null; var template = "Event #Type#, User (id: #UserId#)."; - var expected = $"Event {eventMessage.Type}, User (id: #UserId#)."; + var expected = $"Event {eventMessage.Type}, User (id: )."; var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage); Assert.Equal(expected, result); From 04cb7820a67dc0411b709fb1f6f9ffd73e6a79d0 Mon Sep 17 00:00:00 2001 From: Derek Nance Date: Wed, 10 Sep 2025 16:34:10 -0500 Subject: [PATCH 225/326] [PM-25088] Fix collision with PM-24964 (#6312) `ISetupIntentCache.Remove` (used in #6262) was renamed to `RemoveSetupIntentForSubscriber` with 3dd5acc in #6263. --- .../Commands/CreatePremiumCloudHostedSubscriptionCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs index 8a73f31880..1227cdc034 100644 --- a/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs +++ b/src/Core/Billing/Premium/Commands/CreatePremiumCloudHostedSubscriptionCommand.cs @@ -204,7 +204,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand( { case TokenizablePaymentMethodType.BankAccount: { - await setupIntentCache.Remove(user.Id); + await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id); break; } case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): From bd1745a50d7bb24060b45f57a7604c6cd17066d7 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:37:45 +1000 Subject: [PATCH 226/326] =?UTF-8?q?[PM-24192]=20Add=20OrganizationContext?= =?UTF-8?q?=20in=20API=C2=A0project=20(#6291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...uthorizationHandlerCollectionExtensions.cs | 21 +++ .../Authorization/HttpContextExtensions.cs | 4 +- .../OrganizationClaimsExtensions.cs | 4 +- .../Authorization/OrganizationContext.cs | 84 ++++++++++++ .../Utilities/ServiceCollectionExtensions.cs | 8 +- .../Context/CurrentContextOrganization.cs | 4 + .../Authorization/OrganizationContextTests.cs | 125 ++++++++++++++++++ 7 files changed, 238 insertions(+), 12 deletions(-) create mode 100644 src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs create mode 100644 src/Api/AdminConsole/Authorization/OrganizationContext.cs create mode 100644 test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs diff --git a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs new file mode 100644 index 0000000000..70cbc0d1a4 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Bit.Api.Vault.AuthorizationHandlers.Collections; +using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.Api.AdminConsole.Authorization; + +public static class AuthorizationHandlerCollectionExtensions +{ + public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection services) + { + services.TryAddScoped(); + + services.TryAddEnumerable([ + ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), + ]); + } +} diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs index accb9539fa..5cb261b41d 100644 --- a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs +++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; diff --git a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs index a3af3669ac..9ea01bd21b 100644 --- a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs +++ b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs @@ -1,6 +1,4 @@ -#nullable enable - -using System.Security.Claims; +using System.Security.Claims; using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; diff --git a/src/Api/AdminConsole/Authorization/OrganizationContext.cs b/src/Api/AdminConsole/Authorization/OrganizationContext.cs new file mode 100644 index 0000000000..7b06e33dfd --- /dev/null +++ b/src/Api/AdminConsole/Authorization/OrganizationContext.cs @@ -0,0 +1,84 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Bit.Core.Services; + +// Note: do not move this into Core! See remarks below. +namespace Bit.Api.AdminConsole.Authorization; + +/// +/// Provides information about a user's membership or provider relationship with an organization. +/// Used for authorization decisions in the API layer, usually called by a controller or authorization handler or attribute. +/// +/// +/// This is intended to deprecate organization-related methods in . +/// It should remain in the API layer (not Core) because it is closely tied to user claims and authentication. +/// +public interface IOrganizationContext +{ + /// + /// Parses the provided for claims relating to the specified organization. + /// A user will have organization claims if they are a confirmed member of the organization. + /// + /// The claims for the user. + /// The organization to extract claims for. + /// + /// A representing the user's claims for the organization, + /// or null if the user has no claims. + /// + public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId); + /// + /// Used to determine whether the user is a ProviderUser for the specified organization. + /// + /// The claims for the user. + /// The organization to check the provider relationship for. + /// True if the user is a ProviderUser for the specified organization, otherwise false. + /// + /// This requires a database call, but the results are cached for the lifetime of the service instance. + /// Try to check purely claims-based sources of authorization first (such as organization membership with + /// ) to avoid unnecessary database calls. + /// + public Task IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId); +} + +public class OrganizationContext( + IUserService userService, + IProviderUserRepository providerUserRepository) : IOrganizationContext +{ + public const string NoUserIdError = "This method should only be called on the private api with a logged in user."; + + /// + /// Caches provider relationships by UserId. + /// In practice this should only have 1 entry (for the current user), but this approach ensures that a mix-up + /// between users cannot occur if is called with a different + /// ClaimsPrincipal for any reason. + /// + private readonly Dictionary> _providerUserOrganizationsCache = new(); + + public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId) + { + return user.GetCurrentContextOrganization(organizationId); + } + + public async Task IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId) + { + var userId = userService.GetProperUserId(user); + if (!userId.HasValue) + { + throw new InvalidOperationException(NoUserIdError); + } + + if (!_providerUserOrganizationsCache.TryGetValue(userId.Value, out var providerUserOrganizations)) + { + providerUserOrganizations = + await providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId.Value, + ProviderUserStatusType.Confirmed); + providerUserOrganizations = providerUserOrganizations.ToList(); + _providerUserOrganizationsCache[userId.Value] = providerUserOrganizations; + } + + return providerUserOrganizations.Any(o => o.OrganizationId == organizationId); + } +} diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index b956fc73bb..6af688f548 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -1,7 +1,5 @@ using Bit.Api.AdminConsole.Authorization; using Bit.Api.Tools.Authorization; -using Bit.Api.Vault.AuthorizationHandlers.Collections; -using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.Auth.IdentityServer; using Bit.Core.PhishingDomainFeatures; using Bit.Core.PhishingDomainFeatures.Interfaces; @@ -109,14 +107,12 @@ public static class ServiceCollectionExtensions public static void AddAuthorizationHandlers(this IServiceCollection services) { - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + // Admin Console authorization handlers + services.AddAdminConsoleAuthorizationHandlers(); } public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings) diff --git a/src/Core/AdminConsole/Context/CurrentContextOrganization.cs b/src/Core/AdminConsole/Context/CurrentContextOrganization.cs index 3c9dc10cc0..e154a5a25f 100644 --- a/src/Core/AdminConsole/Context/CurrentContextOrganization.cs +++ b/src/Core/AdminConsole/Context/CurrentContextOrganization.cs @@ -5,6 +5,10 @@ using Bit.Core.Utilities; namespace Bit.Core.Context; +/// +/// Represents the claims for a user in relation to a particular organization. +/// These claims will only be present for users in the status. +/// public class CurrentContextOrganization { public CurrentContextOrganization() { } diff --git a/test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs b/test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs new file mode 100644 index 0000000000..92109cea93 --- /dev/null +++ b/test/Api.Test/AdminConsole/Authorization/OrganizationContextTests.cs @@ -0,0 +1,125 @@ +using System.Security.Claims; +using AutoFixture; +using Bit.Api.AdminConsole.Authorization; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.AdminConsole.Authorization; + +[SutProviderCustomize] +public class OrganizationContextTests +{ + [Theory, BitAutoData] + public async Task IsProviderUserForOrganization_UserIsProviderUser_ReturnsTrue( + Guid userId, Guid organizationId, Guid otherOrganizationId, + SutProvider sutProvider) + { + var claimsPrincipal = new ClaimsPrincipal(); + var providerUserOrganizations = new List + { + new() { OrganizationId = organizationId }, + new() { OrganizationId = otherOrganizationId } + }; + + sutProvider.GetDependency() + .GetProperUserId(claimsPrincipal) + .Returns(userId); + + sutProvider.GetDependency() + .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed) + .Returns(providerUserOrganizations); + + var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId); + + Assert.True(result); + await sutProvider.GetDependency() + .Received(1) + .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed); + } + + public static IEnumerable UserIsNotProviderUserData() + { + // User has provider organizations, but not for the target organization + yield return + [ + new List + { + new Fixture().Create() + } + ]; + + // User has no provider organizations + yield return [Array.Empty()]; + } + + [Theory, BitMemberAutoData(nameof(UserIsNotProviderUserData))] + public async Task IsProviderUserForOrganization_UserIsNotProviderUser_ReturnsFalse( + IEnumerable providerUserOrganizations, + Guid userId, Guid organizationId, + SutProvider sutProvider) + { + var claimsPrincipal = new ClaimsPrincipal(); + + sutProvider.GetDependency() + .GetProperUserId(claimsPrincipal) + .Returns(userId); + + sutProvider.GetDependency() + .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed) + .Returns(providerUserOrganizations); + + var result = await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId); + + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task IsProviderUserForOrganization_UserIdIsNull_ThrowsException( + Guid organizationId, + SutProvider sutProvider) + { + var claimsPrincipal = new ClaimsPrincipal(); + + sutProvider.GetDependency() + .GetProperUserId(claimsPrincipal) + .Returns((Guid?)null); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId)); + + Assert.Equal(OrganizationContext.NoUserIdError, exception.Message); + } + + [Theory, BitAutoData] + public async Task IsProviderUserForOrganization_UsesCaching( + Guid userId, Guid organizationId, + SutProvider sutProvider) + { + var claimsPrincipal = new ClaimsPrincipal(); + var providerUserOrganizations = new List + { + new() { OrganizationId = organizationId } + }; + + sutProvider.GetDependency() + .GetProperUserId(claimsPrincipal) + .Returns(userId); + + sutProvider.GetDependency() + .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed) + .Returns(providerUserOrganizations); + + await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId); + await sutProvider.Sut.IsProviderUserForOrganization(claimsPrincipal, organizationId); + + await sutProvider.GetDependency() + .Received(1) + .GetManyOrganizationDetailsByUserAsync(userId, ProviderUserStatusType.Confirmed); + } +} From 2c860df34bc245a621efb8e7799eef4f68b341ce Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 11 Sep 2025 13:58:32 +1000 Subject: [PATCH 227/326] [PM-15621] Refactor delete claimed user command (#6221) - create vNext command - restructure command to simplify logic - move validation to a separate class - implement result types using OneOf library and demo their use here --- .../OrganizationUsersController.cs | 52 ++ .../OrganizationUserResponseModel.cs | 4 +- .../CommandResult.cs | 42 ++ ...imedOrganizationUserAccountCommandvNext.cs | 137 +++++ ...ClaimedOrganizationUserAccountValidator.cs | 76 +++ .../DeleteUserValidationRequest.cs | 13 + .../DeleteClaimedAccountvNext/Errors.cs | 21 + ...imedOrganizationUserAccountCommandvNext.cs | 17 + ...edOrganizationUserAccountValidatorvNext.cs | 6 + .../ValidationResult.cs | 41 ++ src/Core/Constants.cs | 1 + ...OrganizationServiceCollectionExtensions.cs | 5 + .../OrganizationUserControllerTests.cs | 121 ++++- .../OrganizationUsersControllerTests.cs | 21 - ...rganizationUserAccountCommandvNextTests.cs | 467 ++++++++++++++++ ...anizationUserAccountValidatorvNextTests.cs | 503 ++++++++++++++++++ 16 files changed, 1502 insertions(+), 25 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5183b59b00..16d6984334 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -11,6 +11,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; @@ -23,6 +24,7 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; @@ -59,6 +61,7 @@ public class OrganizationUsersController : Controller private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand; + private readonly IDeleteClaimedOrganizationUserAccountCommandvNext _deleteClaimedOrganizationUserAccountCommandvNext; private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; @@ -87,6 +90,7 @@ public class OrganizationUsersController : Controller IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand, + IDeleteClaimedOrganizationUserAccountCommandvNext deleteClaimedOrganizationUserAccountCommandvNext, IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, @@ -115,6 +119,7 @@ public class OrganizationUsersController : Controller _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand; + _deleteClaimedOrganizationUserAccountCommandvNext = deleteClaimedOrganizationUserAccountCommandvNext; _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; @@ -536,6 +541,12 @@ public class OrganizationUsersController : Controller [Authorize] public async Task DeleteAccount(Guid orgId, Guid id) { + if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)) + { + await DeleteAccountvNext(orgId, id); + return; + } + var currentUser = await _userService.GetUserByPrincipalAsync(User); if (currentUser == null) { @@ -553,10 +564,33 @@ public class OrganizationUsersController : Controller await DeleteAccount(orgId, id); } + private async Task DeleteAccountvNext(Guid orgId, Guid id) + { + var currentUserId = _userService.GetProperUserId(User); + if (currentUserId == null) + { + return TypedResults.Unauthorized(); + } + + var commandResult = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteUserAsync(orgId, id, currentUserId.Value); + + return commandResult.Result.Match( + error => error is NotFoundError + ? TypedResults.NotFound(new ErrorResponseModel(error.Message)) + : TypedResults.BadRequest(new ErrorResponseModel(error.Message)), + TypedResults.Ok + ); + } + [HttpDelete("delete-account")] [Authorize] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { + if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)) + { + return await BulkDeleteAccountvNext(orgId, model); + } + var currentUser = await _userService.GetUserByPrincipalAsync(User); if (currentUser == null) { @@ -577,6 +611,24 @@ public class OrganizationUsersController : Controller return await BulkDeleteAccount(orgId, model); } + private async Task> BulkDeleteAccountvNext(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) + { + var currentUserId = _userService.GetProperUserId(User); + if (currentUserId == null) + { + throw new UnauthorizedAccessException(); + } + + var result = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value); + + var responses = result.Select(r => r.Result.Match( + error => new OrganizationUserBulkResponseModel(r.Id, error.Message), + _ => new OrganizationUserBulkResponseModel(r.Id, string.Empty) + )); + + return new ListResponseModel(responses); + } + [HttpPut("{id}/revoke")] [Authorize] public async Task RevokeAsync(Guid orgId, Guid id) diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 7c31c2ae81..eb810599f3 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -236,8 +236,8 @@ public class OrganizationUserPublicKeyResponseModel : ResponseModel public class OrganizationUserBulkResponseModel : ResponseModel { - public OrganizationUserBulkResponseModel(Guid id, string error, - string obj = "OrganizationBulkConfirmResponseModel") : base(obj) + public OrganizationUserBulkResponseModel(Guid id, string error) + : base("OrganizationBulkConfirmResponseModel") { Id = id; Error = error; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs new file mode 100644 index 0000000000..3dfbe4dbda --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs @@ -0,0 +1,42 @@ +using OneOf; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +/// +/// Represents the result of a command. +/// This is a type that contains an Error if the command execution failed, or the result of the command if it succeeded. +/// +/// The type of the successful result. If there is no successful result (void), use . + +public class CommandResult(OneOf result) : OneOfBase(result) +{ + public bool IsError => IsT0; + public bool IsSuccess => IsT1; + public Error AsError => AsT0; + public T AsSuccess => AsT1; + + public static implicit operator CommandResult(T value) => new(value); + public static implicit operator CommandResult(Error error) => new(error); +} + +/// +/// Represents the result of a command where successful execution returns no value (void). +/// See for more information. +/// +public class CommandResult(OneOf result) : CommandResult(result) +{ + public static implicit operator CommandResult(None none) => new(none); + public static implicit operator CommandResult(Error error) => new(error); +} + +/// +/// A wrapper for with an ID, to identify the result in bulk operations. +/// +public record BulkCommandResult(Guid Id, CommandResult Result); + +/// +/// A wrapper for with an ID, to identify the result in bulk operations. +/// +public record BulkCommandResult(Guid Id, CommandResult Result); + diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs new file mode 100644 index 0000000000..3064a426fa --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs @@ -0,0 +1,137 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +public class DeleteClaimedOrganizationUserAccountCommandvNext( + IUserService userService, + IEventService eventService, + IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, + IOrganizationUserRepository organizationUserRepository, + IUserRepository userRepository, + IPushNotificationService pushService, + ILogger logger, + IDeleteClaimedOrganizationUserAccountValidatorvNext deleteClaimedOrganizationUserAccountValidatorvNext) + : IDeleteClaimedOrganizationUserAccountCommandvNext +{ + public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId) + { + var result = await DeleteManyUsersAsync(organizationId, [organizationUserId], deletingUserId); + return result.Single(); + } + + public async Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid deletingUserId) + { + orgUserIds = orgUserIds.ToList(); + var orgUsers = await organizationUserRepository.GetManyAsync(orgUserIds); + var users = await GetUsersAsync(orgUsers); + var claimedStatuses = await getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, orgUserIds); + + var internalRequests = CreateInternalRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, claimedStatuses); + var validationResults = (await deleteClaimedOrganizationUserAccountValidatorvNext.ValidateAsync(internalRequests)).ToList(); + + var validRequests = validationResults.ValidRequests(); + await CancelPremiumsAsync(validRequests); + await HandleUserDeletionsAsync(validRequests); + await LogDeletedOrganizationUsersAsync(validRequests); + + return validationResults.Select(v => v.Match( + error => new BulkCommandResult(v.Request.OrganizationUserId, error), + _ => new BulkCommandResult(v.Request.OrganizationUserId, new None()) + )); + } + + private static IEnumerable CreateInternalRequests( + Guid organizationId, + Guid deletingUserId, + IEnumerable orgUserIds, + ICollection orgUsers, + IEnumerable users, + IDictionary claimedStatuses) + { + foreach (var orgUserId in orgUserIds) + { + var orgUser = orgUsers.FirstOrDefault(orgUser => orgUser.Id == orgUserId); + var user = users.FirstOrDefault(user => user.Id == orgUser?.UserId); + claimedStatuses.TryGetValue(orgUserId, out var isClaimed); + + yield return new DeleteUserValidationRequest + { + User = user, + OrganizationUserId = orgUserId, + OrganizationUser = orgUser, + IsClaimed = isClaimed, + OrganizationId = organizationId, + DeletingUserId = deletingUserId, + }; + } + } + + private async Task> GetUsersAsync(ICollection orgUsers) + { + var userIds = orgUsers + .Where(orgUser => orgUser.UserId.HasValue) + .Select(orgUser => orgUser.UserId!.Value) + .ToList(); + + return await userRepository.GetManyAsync(userIds); + } + + private async Task LogDeletedOrganizationUsersAsync(IEnumerable requests) + { + var eventDate = DateTime.UtcNow; + + var events = requests + .Select(request => (request.OrganizationUser!, EventType.OrganizationUser_Deleted, (DateTime?)eventDate)) + .ToList(); + + if (events.Count != 0) + { + await eventService.LogOrganizationUserEventsAsync(events); + } + } + + private async Task HandleUserDeletionsAsync(IEnumerable requests) + { + var users = requests + .Select(request => request.User!) + .ToList(); + + if (users.Count == 0) + { + return; + } + + await userRepository.DeleteManyAsync(users); + + foreach (var user in users) + { + await pushService.PushLogOutAsync(user.Id); + } + } + + private async Task CancelPremiumsAsync(IEnumerable requests) + { + var users = requests.Select(request => request.User!); + + foreach (var user in users) + { + try + { + await userService.CancelPremiumAsync(user); + } + catch (GatewayException exception) + { + logger.LogWarning(exception, "Failed to cancel premium subscription for {userId}.", user.Id); + } + } + } +} + diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs new file mode 100644 index 0000000000..7a88841d2f --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs @@ -0,0 +1,76 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext.ValidationResultHelpers; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +public class DeleteClaimedOrganizationUserAccountValidatorvNext( + ICurrentContext currentContext, + IOrganizationUserRepository organizationUserRepository, + IProviderUserRepository providerUserRepository) : IDeleteClaimedOrganizationUserAccountValidatorvNext +{ + public async Task>> ValidateAsync(IEnumerable requests) + { + var tasks = requests.Select(ValidateAsync); + var results = await Task.WhenAll(tasks); + return results; + } + + private async Task> ValidateAsync(DeleteUserValidationRequest request) + { + // Ensure user exists + if (request.User == null || request.OrganizationUser == null) + { + return Invalid(request, new UserNotFoundError()); + } + + // Cannot delete invited users + if (request.OrganizationUser.Status == OrganizationUserStatusType.Invited) + { + return Invalid(request, new InvalidUserStatusError()); + } + + // Cannot delete yourself + if (request.OrganizationUser.UserId == request.DeletingUserId) + { + return Invalid(request, new CannotDeleteYourselfError()); + } + + // Can only delete a claimed user + if (!request.IsClaimed) + { + return Invalid(request, new UserNotClaimedError()); + } + + // Cannot delete an owner unless you are an owner or provider + if (request.OrganizationUser.Type == OrganizationUserType.Owner && + !await currentContext.OrganizationOwner(request.OrganizationId)) + { + return Invalid(request, new CannotDeleteOwnersError()); + } + + // Cannot delete a user who is the sole owner of an organization + var onlyOwnerCount = await organizationUserRepository.GetCountByOnlyOwnerAsync(request.User.Id); + if (onlyOwnerCount > 0) + { + return Invalid(request, new SoleOwnerError()); + } + + // Cannot delete a user who is the sole member of a provider + var onlyOwnerProviderCount = await providerUserRepository.GetCountByOnlyOwnerAsync(request.User.Id); + if (onlyOwnerProviderCount > 0) + { + return Invalid(request, new SoleProviderError()); + } + + // Custom users cannot delete admins + if (request.OrganizationUser.Type == OrganizationUserType.Admin && await currentContext.OrganizationCustom(request.OrganizationId)) + { + return Invalid(request, new CannotDeleteAdminsError()); + } + + return Valid(request); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs new file mode 100644 index 0000000000..5fd95dc73c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +public class DeleteUserValidationRequest +{ + public Guid OrganizationId { get; init; } + public Guid OrganizationUserId { get; init; } + public OrganizationUser? OrganizationUser { get; init; } + public User? User { get; init; } + public Guid DeletingUserId { get; init; } + public bool IsClaimed { get; init; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs new file mode 100644 index 0000000000..d991a882b8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs @@ -0,0 +1,21 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +/// +/// A strongly typed error containing a reason that an action failed. +/// This is used for business logic validation and other expected errors, not exceptions. +/// +public abstract record Error(string Message); +/// +/// An type that maps to a NotFoundResult at the api layer. +/// +/// +public abstract record NotFoundError(string Message) : Error(Message); + +public record UserNotFoundError() : NotFoundError("Invalid user."); +public record UserNotClaimedError() : Error("Member is not claimed by the organization."); +public record InvalidUserStatusError() : Error("You cannot delete a member with Invited status."); +public record CannotDeleteYourselfError() : Error("You cannot delete yourself."); +public record CannotDeleteOwnersError() : Error("Only owners can delete other owners."); +public record SoleOwnerError() : Error("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); +public record SoleProviderError() : Error("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user."); +public record CannotDeleteAdminsError() : Error("Custom users can not delete admins."); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs new file mode 100644 index 0000000000..2c462a2acf --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs @@ -0,0 +1,17 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +public interface IDeleteClaimedOrganizationUserAccountCommandvNext +{ + /// + /// Removes a user from an organization and deletes all of their associated user data. + /// + Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId); + + /// + /// Removes multiple users from an organization and deletes all of their associated user data. + /// + /// + /// An error message for each user that could not be removed, otherwise null. + /// + Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid deletingUserId); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs new file mode 100644 index 0000000000..f6125a0355 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +public interface IDeleteClaimedOrganizationUserAccountValidatorvNext +{ + Task>> ValidateAsync(IEnumerable requests); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs new file mode 100644 index 0000000000..23d2fbb7ce --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs @@ -0,0 +1,41 @@ +using OneOf; +using OneOf.Types; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +/// +/// Represents the result of validating a request. +/// This is for use within the Core layer, e.g. validating a command request. +/// +/// The request that has been validated. +/// A type that contains an Error if validation failed. +/// The request type. +public class ValidationResult(TRequest request, OneOf error) : OneOfBase(error) +{ + public TRequest Request { get; } = request; + + public bool IsError => IsT0; + public bool IsValid => IsT1; + public Error AsError => AsT0; +} + +public static class ValidationResultHelpers +{ + /// + /// Creates a successful with no error set. + /// + public static ValidationResult Valid(T request) => new(request, new None()); + /// + /// Creates a failed with the specified error. + /// + public static ValidationResult Invalid(T request, Error error) => new(request, error); + + /// + /// Extracts successfully validated requests from a sequence of . + /// + public static List ValidRequests(this IEnumerable> results) => + results + .Where(r => r.IsValid) + .Select(r => r.Request) + .ToList(); +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cba060427c..3a825bc533 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -134,6 +134,7 @@ public static class FeatureFlagKeys public const string DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; + public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index bcbaccca7c..1c38a27d1e 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; @@ -133,6 +134,10 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + + // vNext implementations (feature flagged) + services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index 04ab72fad1..b7839467e8 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -1,10 +1,13 @@ using System.Net; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Request; +using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; @@ -30,6 +33,10 @@ public class OrganizationUserControllerTests : IClassFixture(); + var organizationUserRepository = _factory.GetService(); + + Assert.NotNull(orgUserToDelete.UserId); + Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value)); + Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id)); + + var request = new OrganizationUserBulkRequestModel + { + Ids = [orgUserToDelete.Id] + }; + + var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request); + var content = await httpResponse.Content.ReadFromJsonAsync>(); + Assert.Single(content.Data, r => r.Id == orgUserToDelete.Id && r.Error == string.Empty); + + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); + Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value)); + Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id)); + } + + [Fact] + public async Task BulkDeleteAccount_MixedResults() + { + var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.Admin); + + await _loginHelper.LoginAsync(userEmail); + + // Can delete users + var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User); + // Cannot delete owners + var (_, invalidOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner); + await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com"); + + var userRepository = _factory.GetService(); + var organizationUserRepository = _factory.GetService(); + + Assert.NotNull(validOrgUser.UserId); + Assert.NotNull(invalidOrgUser.UserId); + + var arrangedUsers = + await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]); + Assert.Equal(2, arrangedUsers.Count()); + + var arrangedOrgUsers = + await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]); + Assert.Equal(2, arrangedOrgUsers.Count); + + var request = new OrganizationUserBulkRequestModel + { + Ids = [validOrgUser.Id, invalidOrgUser.Id] + }; + + var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request); + + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); + var debug = await httpResponse.Content.ReadAsStringAsync(); + var content = await httpResponse.Content.ReadFromJsonAsync>(); + Assert.Equal(2, content.Data.Count()); + Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty); + Assert.Contains(content.Data, r => + r.Id == invalidOrgUser.Id && + string.Equals(r.Error, new CannotDeleteOwnersError().Message, StringComparison.Ordinal)); + + var actualUsers = + await userRepository.GetManyAsync([validOrgUser.UserId.Value, invalidOrgUser.UserId.Value]); + Assert.Single(actualUsers, u => u.Id == invalidOrgUser.UserId.Value); + + var actualOrgUsers = + await organizationUserRepository.GetManyAsync([validOrgUser.Id, invalidOrgUser.Id]); + Assert.Single(actualOrgUsers, ou => ou.Id == invalidOrgUser.Id); + } + [Theory] [InlineData(OrganizationUserType.User)] [InlineData(OrganizationUserType.Custom)] @@ -57,11 +149,36 @@ public class OrganizationUserControllerTests : IClassFixture { Guid.NewGuid() } }; - var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/remove", request); + var httpResponse = await _client.PostAsJsonAsync($"organizations/{_organization.Id}/users/delete-account", request); Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode); } + [Fact] + public async Task DeleteAccount_Success() + { + var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, + _organization.Id, OrganizationUserType.Owner); + + await _loginHelper.LoginAsync(userEmail); + + var (_, orgUserToDelete) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User); + await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com"); + + var userRepository = _factory.GetService(); + var organizationUserRepository = _factory.GetService(); + + Assert.NotNull(orgUserToDelete.UserId); + Assert.NotNull(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value)); + Assert.NotNull(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id)); + + var httpResponse = await _client.DeleteAsync($"organizations/{_organization.Id}/users/{orgUserToDelete.Id}/delete-account"); + + Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode); + Assert.Null(await userRepository.GetByIdAsync(orgUserToDelete.UserId.Value)); + Assert.Null(await organizationUserRepository.GetByIdAsync(orgUserToDelete.Id)); + } + [Theory] [InlineData(OrganizationUserType.User)] [InlineData(OrganizationUserType.Custom)] @@ -74,7 +191,7 @@ public class OrganizationUserControllerTests : IClassFixture deleteResults, SutProvider sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(true); - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - sutProvider.GetDependency() - .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id) - .Returns(deleteResults); - - var response = await sutProvider.Sut.BulkDeleteAccount(orgId, model); - - Assert.Equal(deleteResults.Count, response.Data.Count()); - Assert.True(response.Data.All(r => deleteResults.Any(res => res.Item1 == r.Id && res.Item2 == r.Error))); - await sutProvider.GetDependency() - .Received(1) - .DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); - } - [Theory] [BitAutoData] public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs new file mode 100644 index 0000000000..679c1914c6 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs @@ -0,0 +1,467 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +[SutProviderCustomize] +public class DeleteClaimedOrganizationUserAccountCommandvNextTests +{ + [Theory] + [BitAutoData] + public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organizationId; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + var validationResult = CreateSuccessfulValidationResult(request); + + SetupRepositoryMocks(sutProvider, + new List { organizationUser }, + [user], + organizationId, + new Dictionary { { organizationUser.Id, true } }); + + SetupValidatorMock(sutProvider, [validationResult]); + + var result = await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUser.Id, deletingUserId); + + Assert.Equal(organizationUser.Id, result.Id); + Assert.True(result.Result.IsSuccess); + + await sutProvider.GetDependency() + .Received(1) + .GetManyAsync(Arg.Is>(ids => ids.Contains(organizationUser.Id))); + + await AssertSuccessfulUserOperations(sutProvider, [user], [organizationUser]); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults( + SutProvider sutProvider, + Guid organizationId, + Guid deletingUserId) + { + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [], deletingUserId); + + Assert.Empty(results); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents( + SutProvider sutProvider, + User user1, + User user2, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser orgUser1, + [OrganizationUser] OrganizationUser orgUser2) + { + // Arrange + orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId; + orgUser1.UserId = user1.Id; + orgUser2.UserId = user2.Id; + + var request1 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUser1.Id, + OrganizationUser = orgUser1, + User = user1, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + var request2 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUser2.Id, + OrganizationUser = orgUser2, + User = user2, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var validationResults = new[] + { + CreateSuccessfulValidationResult(request1), + CreateSuccessfulValidationResult(request2) + }; + + SetupRepositoryMocks(sutProvider, + new List { orgUser1, orgUser2 }, + [user1, user2], + organizationId, + new Dictionary { { orgUser1.Id, true }, { orgUser2.Id, true } }); + + SetupValidatorMock(sutProvider, validationResults); + + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser1.Id, orgUser2.Id], deletingUserId); + + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + Assert.All(resultsList, result => Assert.True(result.Result.IsSuccess)); + + await AssertSuccessfulUserOperations(sutProvider, [user1, user2], [orgUser1, orgUser2]); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults( + SutProvider sutProvider, + Guid organizationId, + Guid orgUserId1, + Guid orgUserId2, + Guid deletingUserId) + { + // Arrange + var request1 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUserId1, + DeletingUserId = deletingUserId + }; + var request2 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUserId2, + DeletingUserId = deletingUserId + }; + + var validationResults = new[] + { + CreateFailedValidationResult(request1, new UserNotClaimedError()), + CreateFailedValidationResult(request2, new InvalidUserStatusError()) + }; + + SetupRepositoryMocks(sutProvider, [], [], organizationId, new Dictionary()); + SetupValidatorMock(sutProvider, validationResults); + + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserId1, orgUserId2], deletingUserId); + + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + Assert.Equal(orgUserId1, resultsList[0].Id); + Assert.True(resultsList[0].Result.IsError); + Assert.IsType(resultsList[0].Result.AsError); + + Assert.Equal(orgUserId2, resultsList[1].Id); + Assert.True(resultsList[1].Result.IsError); + Assert.IsType(resultsList[1].Result.AsError); + + await AssertNoUserOperations(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly( + SutProvider sutProvider, + User validUser, + Guid organizationId, + Guid validOrgUserId, + Guid invalidOrgUserId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser validOrgUser) + { + validOrgUser.Id = validOrgUserId; + validOrgUser.UserId = validUser.Id; + validOrgUser.OrganizationId = organizationId; + + var validRequest = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = validOrgUserId, + OrganizationUser = validOrgUser, + User = validUser, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + var invalidRequest = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = invalidOrgUserId, + DeletingUserId = deletingUserId + }; + + var validationResults = new[] + { + CreateSuccessfulValidationResult(validRequest), + CreateFailedValidationResult(invalidRequest, new UserNotFoundError()) + }; + + SetupRepositoryMocks(sutProvider, + new List { validOrgUser }, + [validUser], + organizationId, + new Dictionary { { validOrgUserId, true } }); + + SetupValidatorMock(sutProvider, validationResults); + + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [validOrgUserId, invalidOrgUserId], deletingUserId); + + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + var validResult = resultsList.First(r => r.Id == validOrgUserId); + var invalidResult = resultsList.First(r => r.Id == invalidOrgUserId); + + Assert.True(validResult.Result.IsSuccess); + Assert.True(invalidResult.Result.IsError); + Assert.IsType(invalidResult.Result.AsError); + + await AssertSuccessfulUserOperations(sutProvider, [validUser], [validOrgUser]); + } + + [Theory] + [BitAutoData] + public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser orgUser) + { + orgUser.UserId = user.Id; + orgUser.OrganizationId = organizationId; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUser.Id, + OrganizationUser = orgUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + var validationResult = CreateSuccessfulValidationResult(request); + + SetupRepositoryMocks(sutProvider, + new List { orgUser }, + [user], + organizationId, + new Dictionary { { orgUser.Id, true } }); + + SetupValidatorMock(sutProvider, [validationResult]); + + var gatewayException = new GatewayException("Payment gateway error"); + sutProvider.GetDependency() + .CancelPremiumAsync(user) + .ThrowsAsync(gatewayException); + + var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUser.Id], deletingUserId); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList.First().Result.IsSuccess); + + await sutProvider.GetDependency().Received(1).CancelPremiumAsync(user); + await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]); + + sutProvider.GetDependency>() + .Received(1) + .Log( + LogLevel.Warning, + Arg.Any(), + Arg.Is(o => o.ToString()!.Contains($"Failed to cancel premium subscription for {user.Id}")), + gatewayException, + Arg.Any>()); + } + + + [Theory] + [BitAutoData] + public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers( + SutProvider sutProvider, + User user1, + User user2, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser orgUser1, + [OrganizationUser] OrganizationUser orgUser2) + { + orgUser1.UserId = user1.Id; + orgUser2.UserId = user2.Id; + var orgUserIds = new[] { orgUser1.Id, orgUser2.Id }; + var orgUsers = new List { orgUser1, orgUser2 }; + var users = new[] { user1, user2 }; + var claimedStatuses = new Dictionary { { orgUser1.Id, true }, { orgUser2.Id, false } }; + + sutProvider.GetDependency() + .GetManyAsync(Arg.Any>()) + .Returns(orgUsers); + + sutProvider.GetDependency() + .GetManyAsync(Arg.Is>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id))) + .Returns(users); + + sutProvider.GetDependency() + .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any>()) + .Returns(claimedStatuses); + + sutProvider.GetDependency() + .ValidateAsync(Arg.Any>()) + .Returns(callInfo => + { + var requests = callInfo.Arg>(); + return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError())); + }); + + // Act + await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ValidateAsync(Arg.Is>(requests => + requests.Count() == 2 && + requests.Any(r => r.OrganizationUserId == orgUser1.Id && + r.OrganizationId == organizationId && + r.OrganizationUser == orgUser1 && + r.User == user1 && + r.DeletingUserId == deletingUserId && + r.IsClaimed == true) && + requests.Any(r => r.OrganizationUserId == orgUser2.Id && + r.OrganizationId == organizationId && + r.OrganizationUser == orgUser2 && + r.User == user2 && + r.DeletingUserId == deletingUserId && + r.IsClaimed == false))); + } + + [Theory] + [BitAutoData] + public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection( + SutProvider sutProvider, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser orgUserWithoutUserId) + { + orgUserWithoutUserId.UserId = null; // Intentionally setting to null for test case + + sutProvider.GetDependency() + .GetManyAsync(Arg.Any>()) + .Returns(new List { orgUserWithoutUserId }); + + sutProvider.GetDependency() + .GetManyAsync(Arg.Is>(ids => !ids.Any())) + .Returns([]); + + sutProvider.GetDependency() + .ValidateAsync(Arg.Any>()) + .Returns(callInfo => + { + var requests = callInfo.Arg>(); + return requests.Select(r => CreateFailedValidationResult(r, new UserNotFoundError())); + }); + + // Act + await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .ValidateAsync(Arg.Is>(requests => + requests.Count() == 1 && + requests.Single().User == null)); + + await sutProvider.GetDependency().Received(1) + .GetManyAsync(Arg.Is>(ids => !ids.Any())); + } + + private static ValidationResult CreateSuccessfulValidationResult( + DeleteUserValidationRequest request) => + ValidationResultHelpers.Valid(request); + + private static ValidationResult CreateFailedValidationResult( + DeleteUserValidationRequest request, + Error error) => + ValidationResultHelpers.Invalid(request, error); + + private static void SetupRepositoryMocks( + SutProvider sutProvider, + ICollection orgUsers, + IEnumerable users, + Guid organizationId, + Dictionary claimedStatuses) + { + sutProvider.GetDependency() + .GetManyAsync(Arg.Any>()) + .Returns(orgUsers); + + sutProvider.GetDependency() + .GetManyAsync(Arg.Any>()) + .Returns(users); + + sutProvider.GetDependency() + .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any>()) + .Returns(claimedStatuses); + } + + private static void SetupValidatorMock( + SutProvider sutProvider, + IEnumerable> validationResults) + { + sutProvider.GetDependency() + .ValidateAsync(Arg.Any>()) + .Returns(validationResults); + } + + private static async Task AssertSuccessfulUserOperations( + SutProvider sutProvider, + IEnumerable expectedUsers, + IEnumerable expectedOrgUsers) + { + var userList = expectedUsers.ToList(); + var orgUserList = expectedOrgUsers.ToList(); + + await sutProvider.GetDependency().Received(1) + .DeleteManyAsync(Arg.Is>(users => + userList.All(expectedUser => users.Any(u => u.Id == expectedUser.Id)))); + + foreach (var user in userList) + { + await sutProvider.GetDependency().Received(1).PushLogOutAsync(user.Id); + } + + await sutProvider.GetDependency().Received(1) + .LogOrganizationUserEventsAsync(Arg.Is>(events => + orgUserList.All(expectedOrgUser => + events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted)))); + } + + private static async Task AssertNoUserOperations(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteManyAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushLogOutAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventsAsync(default(IEnumerable<(OrganizationUser, EventType, DateTime?)>)); + } +} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs new file mode 100644 index 0000000000..e51df6a626 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs @@ -0,0 +1,503 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; + +[SutProviderCustomize] +public class DeleteClaimedOrganizationUserAccountValidatorvNextTests +{ + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + organizationUser.OrganizationId = organizationId; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsValid); + Assert.Equal(request, resultsList[0].Request); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults( + SutProvider sutProvider, + User user1, + User user2, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser2) + { + orgUser1.UserId = user1.Id; + orgUser1.OrganizationId = organizationId; + + orgUser2.UserId = user2.Id; + orgUser2.OrganizationId = organizationId; + + var request1 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUser1.Id, + OrganizationUser = orgUser1, + User = user1, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var request2 = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = orgUser2.Id, + OrganizationUser = orgUser2, + User = user2, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user1.Id); + SetupMocks(sutProvider, organizationId, user2.Id); + + var results = await sutProvider.Sut.ValidateAsync([request1, request2]); + + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + Assert.All(resultsList, result => Assert.True(result.IsValid)); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError( + SutProvider sutProvider, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = null, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId) + { + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = Guid.NewGuid(), + OrganizationUser = null, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError( + SutProvider sutProvider, + User user, + Guid organizationId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = user.Id, + IsClaimed = true + }; + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = false + }; + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id); + + sutProvider.GetDependency() + .GetCountByOnlyOwnerAsync(user.Id) + .Returns(1); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id); + + sutProvider.GetDependency() + .GetCountByOnlyOwnerAsync(user.Id) + .Returns(1); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Custom); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsError); + Assert.IsType(resultsList[0].AsError); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult( + SutProvider sutProvider, + User user, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser) + { + organizationUser.UserId = user.Id; + + var request = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = organizationUser.Id, + OrganizationUser = organizationUser, + User = user, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, user.Id, OrganizationUserType.Admin); + + var results = await sutProvider.Sut.ValidateAsync([request]); + + var resultsList = results.ToList(); + Assert.Single(resultsList); + Assert.True(resultsList[0].IsValid); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults( + SutProvider sutProvider, + User validUser, + User invalidUser, + Guid organizationId, + Guid deletingUserId, + [OrganizationUser] OrganizationUser validOrgUser, + [OrganizationUser(OrganizationUserStatusType.Invited)] OrganizationUser invalidOrgUser) + { + validOrgUser.UserId = validUser.Id; + + invalidOrgUser.UserId = invalidUser.Id; + + var validRequest = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = validOrgUser.Id, + OrganizationUser = validOrgUser, + User = validUser, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + var invalidRequest = new DeleteUserValidationRequest + { + OrganizationId = organizationId, + OrganizationUserId = invalidOrgUser.Id, + OrganizationUser = invalidOrgUser, + User = invalidUser, + DeletingUserId = deletingUserId, + IsClaimed = true + }; + + SetupMocks(sutProvider, organizationId, validUser.Id); + + var results = await sutProvider.Sut.ValidateAsync([validRequest, invalidRequest]); + + var resultsList = results.ToList(); + Assert.Equal(2, resultsList.Count); + + var validResult = resultsList.First(r => r.Request == validRequest); + var invalidResult = resultsList.First(r => r.Request == invalidRequest); + + Assert.True(validResult.IsValid); + Assert.True(invalidResult.IsError); + Assert.IsType(invalidResult.AsError); + } + + private static void SetupMocks( + SutProvider sutProvider, + Guid organizationId, + Guid userId, + OrganizationUserType currentUserType = OrganizationUserType.Owner) + { + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(currentUserType == OrganizationUserType.Owner); + + sutProvider.GetDependency() + .OrganizationAdmin(organizationId) + .Returns(currentUserType is OrganizationUserType.Owner or OrganizationUserType.Admin); + + sutProvider.GetDependency() + .OrganizationCustom(organizationId) + .Returns(currentUserType is OrganizationUserType.Custom); + + sutProvider.GetDependency() + .GetCountByOnlyOwnerAsync(userId) + .Returns(0); + + sutProvider.GetDependency() + .GetCountByOnlyOwnerAsync(userId) + .Returns(0); + } +} From 51c9958ff1276483f08e8a67fd650d03b88da01e Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:21:04 -0500 Subject: [PATCH 228/326] update global settings for icons service so URIs are available internally (#6303) --- src/Icons/appsettings.Development.json | 17 +++++++++++++++++ src/Icons/appsettings.Production.json | 17 +++++++++++++++++ src/Icons/appsettings.QA.json | 17 +++++++++++++++++ src/Icons/appsettings.SelfHosted.json | 19 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/Icons/appsettings.SelfHosted.json diff --git a/src/Icons/appsettings.Development.json b/src/Icons/appsettings.Development.json index fa8ce71a97..b7d7186ffa 100644 --- a/src/Icons/appsettings.Development.json +++ b/src/Icons/appsettings.Development.json @@ -1,4 +1,21 @@ { + "globalSettings": { + "baseServiceUri": { + "vault": "https://localhost:8080", + "api": "http://localhost:4000", + "identity": "http://localhost:33656", + "admin": "http://localhost:62911", + "notifications": "http://localhost:61840", + "sso": "http://localhost:51822", + "internalNotifications": "http://localhost:61840", + "internalAdmin": "http://localhost:62911", + "internalIdentity": "http://localhost:33656", + "internalApi": "http://localhost:4000", + "internalVault": "https://localhost:8080", + "internalSso": "http://localhost:51822", + "internalScim": "http://localhost:44559" + } + }, "Logging": { "IncludeScopes": false, "LogLevel": { diff --git a/src/Icons/appsettings.Production.json b/src/Icons/appsettings.Production.json index 437045a7fb..828e8c61cc 100644 --- a/src/Icons/appsettings.Production.json +++ b/src/Icons/appsettings.Production.json @@ -1,4 +1,21 @@ { + "globalSettings": { + "baseServiceUri": { + "vault": "https://vault.bitwarden.com", + "api": "https://api.bitwarden.com", + "identity": "https://identity.bitwarden.com", + "admin": "https://admin.bitwarden.com", + "notifications": "https://notifications.bitwarden.com", + "sso": "https://sso.bitwarden.com", + "internalNotifications": "https://notifications.bitwarden.com", + "internalAdmin": "https://admin.bitwarden.com", + "internalIdentity": "https://identity.bitwarden.com", + "internalApi": "https://api.bitwarden.com", + "internalVault": "https://vault.bitwarden.com", + "internalSso": "https://sso.bitwarden.com", + "internalScim": "https://scim.bitwarden.com" + } + }, "Logging": { "IncludeScopes": false, "LogLevel": { diff --git a/src/Icons/appsettings.QA.json b/src/Icons/appsettings.QA.json index aec6c424af..ad323c8af6 100644 --- a/src/Icons/appsettings.QA.json +++ b/src/Icons/appsettings.QA.json @@ -1,4 +1,21 @@ { + "globalSettings": { + "baseServiceUri": { + "vault": "https://vault.qa.bitwarden.pw", + "api": "https://api.qa.bitwarden.pw", + "identity": "https://identity.qa.bitwarden.pw", + "admin": "https://admin.qa.bitwarden.pw", + "notifications": "https://notifications.qa.bitwarden.pw", + "sso": "https://sso.qa.bitwarden.pw", + "internalNotifications": "https://notifications.qa.bitwarden.pw", + "internalAdmin": "https://admin.qa.bitwarden.pw", + "internalIdentity": "https://identity.qa.bitwarden.pw", + "internalApi": "https://api.qa.bitwarden.pw", + "internalVault": "https://vault.qa.bitwarden.pw", + "internalSso": "https://sso.qa.bitwarden.pw", + "internalScim": "https://scim.qa.bitwarden.pw" + } + }, "Logging": { "IncludeScopes": false, "LogLevel": { diff --git a/src/Icons/appsettings.SelfHosted.json b/src/Icons/appsettings.SelfHosted.json new file mode 100644 index 0000000000..37faf24b59 --- /dev/null +++ b/src/Icons/appsettings.SelfHosted.json @@ -0,0 +1,19 @@ +{ + "globalSettings": { + "baseServiceUri": { + "vault": null, + "api": null, + "identity": null, + "admin": null, + "notifications": null, + "sso": null, + "internalNotifications": null, + "internalAdmin": null, + "internalIdentity": null, + "internalApi": null, + "internalVault": null, + "internalSso": null, + "internalScim": null + } + } +} From aab50ef5c4753b0a25d43202d45afadd8fa1a5c9 Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:25:57 -0500 Subject: [PATCH 229/326] [PM-24595] [PM-24596] Remove feature flag usage/definition for deleting users with no mp on import (#6313) * chore: remove dc prevent non-mp users from being deleted feature flag, refs PM-24596 * chore: format, refs PM-24596 --- .../Import/ImportOrganizationUsersAndGroupsCommand.cs | 8 ++------ src/Core/Constants.cs | 1 - .../ImportOrganizationUsersAndGroupsCommandTests.cs | 9 --------- .../ImportOrganizationUsersAndGroupsCommandTests.cs | 2 -- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs index 87c6ddea6f..a78dd95260 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommand.cs @@ -22,7 +22,6 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA private readonly IGroupRepository _groupRepository; private readonly IEventService _eventService; private readonly IOrganizationService _organizationService; - private readonly IFeatureService _featureService; private readonly EventSystemUser _EventSystemUser = EventSystemUser.PublicApi; @@ -31,8 +30,7 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA IPaymentService paymentService, IGroupRepository groupRepository, IEventService eventService, - IOrganizationService organizationService, - IFeatureService featureService) + IOrganizationService organizationService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -40,7 +38,6 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA _groupRepository = groupRepository; _eventService = eventService; _organizationService = organizationService; - _featureService = featureService; } /// @@ -238,8 +235,7 @@ public class ImportOrganizationUsersAndGroupsCommand : IImportOrganizationUsersA importUserData.ExistingExternalUsersIdDict.ContainsKey(u.ExternalId)) .ToList(); - if (_featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) && - usersToDelete.Any(u => !u.HasMasterPassword)) + if (usersToDelete.Any(u => !u.HasMasterPassword)) { // Removing users without an MP will put their account in an unrecoverable state. // We allow this during normal syncs for offboarding, but overwriteExisting risks bricking every user in diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3a825bc533..bef947b2b7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -131,7 +131,6 @@ 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 DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; diff --git a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs index 2aea7ac4cd..32c7f75a2b 100644 --- a/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -2,14 +2,11 @@ using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Enums; using Bit.Core.Repositories; -using Bit.Core.Services; -using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.AdminConsole.Import; @@ -25,12 +22,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests : IClassFixture - { - featureService.IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) - .Returns(true); - }); _client = _factory.CreateClient(); _loginHelper = new LoginHelper(_factory, _client); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs index bff1af1cde..933bcbc3a1 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Import/ImportOrganizationUsersAndGroupsCommandTests.cs @@ -98,8 +98,6 @@ public class ImportOrganizationUsersAndGroupsCommandTests SetupOrganizationConfigForImport(sutProvider, org, existingUsers, []); // Existing user does not have a master password - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.DirectoryConnectorPreventUserRemoval) - .Returns(true); existingUsers.First().HasMasterPassword = false; sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); From c2cf29005406e149118b9b14740a67c1add6925e Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:04:05 +0100 Subject: [PATCH 230/326] [PM-21938] Fix: Invoice Payment Issues After Payment Method Updates (#6306) * Resolve the unpaid issue after valid payment method is added * Removed the draft status * Remove draft from the logger msg --- .../Implementations/SubscriberService.cs | 45 +++---------------- .../Services/SubscriberServiceTests.cs | 20 ++------- 2 files changed, 11 insertions(+), 54 deletions(-) diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 1206397d9e..8e75bf3dca 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -580,11 +580,6 @@ public class SubscriberService( PaymentMethod = token }); - var getExistingSetupIntentsForCustomer = stripeAdapter.SetupIntentList(new SetupIntentListOptions - { - Customer = subscriber.GatewayCustomerId - }); - // Find the setup intent for the incoming payment method token. var setupIntentsForUpdatedPaymentMethod = await getSetupIntentsForUpdatedPaymentMethod; @@ -597,24 +592,15 @@ public class SubscriberService( var matchingSetupIntent = setupIntentsForUpdatedPaymentMethod.First(); - // Find the customer's existing setup intents that should be canceled. - var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) - .Where(si => - si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); - // Store the incoming payment method's setup intent ID in the cache for the subscriber so it can be verified later. await setupIntentCache.Set(subscriber.Id, matchingSetupIntent.Id); - // Cancel the customer's other open setup intents. - var postProcessing = existingSetupIntentsForCustomer.Select(si => - stripeAdapter.SetupIntentCancel(si.Id, - new SetupIntentCancelOptions { CancellationReason = "abandoned" })).ToList(); - // Remove the customer's other attached Stripe payment methods. - postProcessing.Add(RemoveStripePaymentMethodsAsync(customer)); - - // Remove the customer's Braintree customer ID. - postProcessing.Add(RemoveBraintreeCustomerIdAsync(customer)); + var postProcessing = new List + { + RemoveStripePaymentMethodsAsync(customer), + RemoveBraintreeCustomerIdAsync(customer) + }; await Task.WhenAll(postProcessing); @@ -622,11 +608,6 @@ public class SubscriberService( } case PaymentMethodType.Card: { - var getExistingSetupIntentsForCustomer = stripeAdapter.SetupIntentList(new SetupIntentListOptions - { - Customer = subscriber.GatewayCustomerId - }); - // Remove the customer's other attached Stripe payment methods. await RemoveStripePaymentMethodsAsync(customer); @@ -634,16 +615,6 @@ public class SubscriberService( await stripeAdapter.PaymentMethodAttachAsync(token, new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - // Find the customer's existing setup intents that should be canceled. - var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) - .Where(si => - si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); - - // Cancel the customer's other open setup intents. - var postProcessing = existingSetupIntentsForCustomer.Select(si => - stripeAdapter.SetupIntentCancel(si.Id, - new SetupIntentCancelOptions { CancellationReason = "abandoned" })).ToList(); - var metadata = customer.Metadata; if (metadata.TryGetValue(BraintreeCustomerIdKey, out var value)) @@ -653,16 +624,14 @@ public class SubscriberService( } // Set the customer's default payment method in Stripe and remove their Braintree customer ID. - postProcessing.Add(stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions + await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions { InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = token }, Metadata = metadata - })); - - await Task.WhenAll(postProcessing); + }); break; } diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index de8c6aae19..2569ffff00 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1320,12 +1320,6 @@ public class SubscriberServiceTests stripeAdapter.SetupIntentList(Arg.Is(options => options.PaymentMethod == "TOKEN")) .Returns([matchingSetupIntent]); - stripeAdapter.SetupIntentList(Arg.Is(options => options.Customer == provider.GatewayCustomerId)) - .Returns([ - new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" }, - new SetupIntent { Id = "setup_intent_3", Status = "succeeded" } - ]); - stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([ new PaymentMethod { Id = "payment_method_1" } ]); @@ -1335,8 +1329,8 @@ public class SubscriberServiceTests await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_1"); - await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2", - Arg.Is(options => options.CancellationReason == "abandoned")); + await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any(), + Arg.Any()); await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1"); @@ -1364,12 +1358,6 @@ public class SubscriberServiceTests } }); - stripeAdapter.SetupIntentList(Arg.Is(options => options.Customer == provider.GatewayCustomerId)) - .Returns([ - new SetupIntent { Id = "setup_intent_2", Status = "requires_payment_method" }, - new SetupIntent { Id = "setup_intent_3", Status = "succeeded" } - ]); - stripeAdapter.CustomerListPaymentMethods(provider.GatewayCustomerId).Returns([ new PaymentMethod { Id = "payment_method_1" } ]); @@ -1377,8 +1365,8 @@ public class SubscriberServiceTests await sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN")); - await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2", - Arg.Is(options => options.CancellationReason == "abandoned")); + await stripeAdapter.DidNotReceive().SetupIntentCancel(Arg.Any(), + Arg.Any()); await stripeAdapter.Received(1).PaymentMethodDetachAsync("payment_method_1"); From ba57ca5f6769d6e1edbd57702f1e2a96352f904a Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:04:37 -0600 Subject: [PATCH 231/326] BRE-1075: Migrate k6 loadtests to Datadog (#6293) * Remove external loadImpact option that is being replaced by DataDog * Add load test workflow Keep otel encrypted, but skip verification Go back to what was working from Billing-Relay Tune test configuration based on last test output. Tune config loadtest Tune tests a bit more by removing preAllocatedVUs Revert "Tune tests a bit more by removing preAllocatedVUs" This reverts commit ab1d170e7a3a6b4296f2c44ed741656a75979c80. Revert "Tune config loadtest" This reverts commit 5bbd551421658e8eb0e2651fb1e005c7f1d52c99. Tune config.js by reducing the amount of pAV Revert "Tune config.js by reducing the amount of pAV" This reverts commit 1e238d335c27ebf46992541ca3733178e165b3aa. Drop MaxVUs * Update .github/workflows/load-test.yml Co-authored-by: Matt Bishop * Fix newline at end of load-test.yml file * Fix github PR accepted code suggestion --------- Co-authored-by: Matt Bishop --- .github/workflows/load-test.yml | 113 ++++++++++++++++++++++++++++++-- perf/load/config.js | 6 -- perf/load/groups.js | 6 -- perf/load/login.js | 6 -- perf/load/sync.js | 6 -- 5 files changed, 106 insertions(+), 31 deletions(-) diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 19aab89be3..c582e6ba00 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -1,13 +1,112 @@ -name: Test Stub +name: Load test + on: + schedule: + - cron: "0 0 * * 1" # Run every Monday at 00:00 workflow_dispatch: + inputs: + test-id: + type: string + description: "Identifier label for Datadog metrics" + default: "server-load-test" + k6-test-path: + type: string + description: "Path to load test files" + default: "perf/load/*.js" + k6-flags: + type: string + description: "Additional k6 flags" + api-env-url: + type: string + description: "URL of the API environment" + default: "https://api.qa.bitwarden.pw" + identity-env-url: + type: string + description: "URL of the Identity environment" + default: "https://identity.qa.bitwarden.pw" + +permissions: + contents: read + id-token: write + +env: + # Secret configuration + AZURE_KEY_VAULT_NAME: gh-server + AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH + # Specify defaults for scheduled runs + TEST_ID: ${{ inputs.test-id || 'server-load-test' }} + K6_TEST_PATH: ${{ inputs.k6-test-path || 'test/load/*.js' }} + API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }} + IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }} jobs: - test: - permissions: - contents: read - name: Test + run-tests: + name: Run load tests runs-on: ubuntu-24.04 steps: - - name: Test - run: exit 0 + - name: Log in to Azure + uses: bitwarden/gh-actions/azure-login@main + with: + subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + tenant_id: ${{ secrets.AZURE_TENANT_ID }} + client_id: ${{ secrets.AZURE_CLIENT_ID }} + + - name: Get Azure Key Vault secrets + id: get-kv-secrets + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: ${{ env.AZURE_KEY_VAULT_NAME }} + secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }} + + - name: Log out of Azure + uses: bitwarden/gh-actions/azure-logout@main + + # Datadog agent for collecting OTEL metrics from k6 + - name: Start Datadog agent + 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_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \ + -e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \ + -e DD_HEALTH_PORT=5555 \ + -e HOST_PROC=/proc \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + --volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ + --health-cmd "curl -f http://localhost:5555/health || exit 1" \ + --health-interval 10s \ + --health-timeout 5s \ + --health-retries 10 \ + --health-start-period 30s \ + --pid host \ + datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479 + + - name: Check out repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Set up k6 + uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0 + + - name: Run k6 tests + uses: grafana/run-k6-action@c6b79182b9b666aa4f630f4a6be9158ead62536e # v1.2.0 + continue-on-error: false + env: + K6_OTEL_METRIC_PREFIX: k6_ + K6_OTEL_GRPC_EXPORTER_INSECURE: true + # Load test specific environment variables + API_URL: ${{ env.API_ENV_URL }} + IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }} + CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }} + AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }} + AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }} + with: + flags: >- + --tag test-id=${{ env.TEST_ID }} + -o experimental-opentelemetry + ${{ inputs.k6-flags }} + path: ${{ env.K6_TEST_PATH }} diff --git a/perf/load/config.js b/perf/load/config.js index f4e1b33bc0..ab7bb8d2fa 100644 --- a/perf/load/config.js +++ b/perf/load/config.js @@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL; const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH; export const options = { - ext: { - loadimpact: { - projectID: 3639465, - name: "Config", - }, - }, scenarios: { constant_load: { executor: "constant-arrival-rate", diff --git a/perf/load/groups.js b/perf/load/groups.js index aee3b3e94d..71e8decdcb 100644 --- a/perf/load/groups.js +++ b/perf/load/groups.js @@ -10,12 +10,6 @@ const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID; const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET; export const options = { - ext: { - loadimpact: { - projectID: 3639465, - name: "Groups", - }, - }, scenarios: { constant_load: { executor: "constant-arrival-rate", diff --git a/perf/load/login.js b/perf/load/login.js index 096974f599..d45b86da5f 100644 --- a/perf/load/login.js +++ b/perf/load/login.js @@ -6,12 +6,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL; const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH; export const options = { - ext: { - loadimpact: { - projectID: 3639465, - name: "Login", - }, - }, scenarios: { constant_load: { executor: "constant-arrival-rate", diff --git a/perf/load/sync.js b/perf/load/sync.js index 5624803e84..2eb2a54403 100644 --- a/perf/load/sync.js +++ b/perf/load/sync.js @@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL; const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH; export const options = { - ext: { - loadimpact: { - projectID: 3639465, - name: "Sync", - }, - }, scenarios: { constant_load: { executor: "constant-arrival-rate", From 7eb5035d94ed67927d3f638ebd34d89003507441 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 12 Sep 2025 10:53:11 -0400 Subject: [PATCH 232/326] [PM-22740] Update current context to jive with Send Access Tokens (#6307) * feat: modify current context to not include user information * fix: circular dependency for feature check in current context. Successfully tested client isn't affected with feature flag off. * test: whole bunch of tests for current context --- src/Core/Context/CurrentContext.cs | 39 +- test/Core.Test/Context/CurrentContextTests.cs | 733 ++++++++++++++++++ 2 files changed, 754 insertions(+), 18 deletions(-) create mode 100644 test/Core.Test/Context/CurrentContextTests.cs diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs index e824a30a0e..5d9b5a1759 100644 --- a/src/Core/Context/CurrentContext.cs +++ b/src/Core/Context/CurrentContext.cs @@ -18,10 +18,10 @@ using Microsoft.AspNetCore.Http; namespace Bit.Core.Context; -public class CurrentContext : ICurrentContext +public class CurrentContext( + IProviderOrganizationRepository _providerOrganizationRepository, + IProviderUserRepository _providerUserRepository) : ICurrentContext { - private readonly IProviderOrganizationRepository _providerOrganizationRepository; - private readonly IProviderUserRepository _providerUserRepository; private bool _builtHttpContext; private bool _builtClaimsPrincipal; private IEnumerable _providerOrganizationProviderDetails; @@ -48,14 +48,6 @@ public class CurrentContext : ICurrentContext public virtual IdentityClientType IdentityClientType { get; set; } public virtual Guid? ServiceAccountOrganizationId { get; set; } - public CurrentContext( - IProviderOrganizationRepository providerOrganizationRepository, - IProviderUserRepository providerUserRepository) - { - _providerOrganizationRepository = providerOrganizationRepository; - _providerUserRepository = providerUserRepository; - } - public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings) { if (_builtHttpContext) @@ -137,6 +129,24 @@ public class CurrentContext : ICurrentContext var claimsDict = user.Claims.GroupBy(c => c.Type).ToDictionary(c => c.Key, c => c.Select(v => v)); + ClientId = GetClaimValue(claimsDict, "client_id"); + + var clientType = GetClaimValue(claimsDict, Claims.Type); + if (clientType != null) + { + if (Enum.TryParse(clientType, out IdentityClientType c)) + { + IdentityClientType = c; + } + } + + if (IdentityClientType == IdentityClientType.Send) + { + // For the Send client, we don't need to set any User specific properties on the context + // so just short circuit and return here. + return Task.FromResult(0); + } + var subject = GetClaimValue(claimsDict, "sub"); if (Guid.TryParse(subject, out var subIdGuid)) { @@ -165,13 +175,6 @@ public class CurrentContext : ICurrentContext } } - var clientType = GetClaimValue(claimsDict, Claims.Type); - if (clientType != null) - { - Enum.TryParse(clientType, out IdentityClientType c); - IdentityClientType = c; - } - if (IdentityClientType == IdentityClientType.ServiceAccount) { ServiceAccountOrganizationId = new Guid(GetClaimValue(claimsDict, Claims.Organization)); diff --git a/test/Core.Test/Context/CurrentContextTests.cs b/test/Core.Test/Context/CurrentContextTests.cs new file mode 100644 index 0000000000..b868d6ceaa --- /dev/null +++ b/test/Core.Test/Context/CurrentContextTests.cs @@ -0,0 +1,733 @@ +using System.Security.Claims; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Http; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Context; + +[SutProviderCustomize] +public class CurrentContextTests +{ + #region BuildAsync(HttpContext) Tests + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsHttpContext( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(httpContext, sutProvider.Sut.HttpContext); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_OnlyBuildsOnce( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + var firstContext = sutProvider.Sut.HttpContext; + + var secondHttpContext = new DefaultHttpContext(); + + // Act + await sutProvider.Sut.BuildAsync(secondHttpContext, globalSettings); + + // Assert + Assert.Equal(firstContext, sutProvider.Sut.HttpContext); + Assert.NotEqual(secondHttpContext, sutProvider.Sut.HttpContext); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsDeviceIdentifier( + SutProvider sutProvider, + string expectedValue) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + sutProvider.Sut.DeviceIdentifier = null; + // Arrange + httpContext.Request.Headers["Device-Identifier"] = expectedValue; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(expectedValue, sutProvider.Sut.DeviceIdentifier); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsCountryName( + SutProvider sutProvider, + string countryName) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + httpContext.Request.Headers["country-name"] = countryName; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(countryName, sutProvider.Sut.CountryName); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsDeviceType( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + var deviceType = DeviceType.Android; + httpContext.Request.Headers["Device-Type"] = ((int)deviceType).ToString(); + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(deviceType, sutProvider.Sut.DeviceType); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsCloudflareFlags( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + sutProvider.Sut.BotScore = null; + // Arrange + var botScore = 85; + httpContext.Request.Headers["X-Cf-Bot-Score"] = botScore.ToString(); + httpContext.Request.Headers["X-Cf-Worked-Proxied"] = "1"; + httpContext.Request.Headers["X-Cf-Is-Bot"] = "1"; + httpContext.Request.Headers["X-Cf-Maybe-Bot"] = "1"; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.True(sutProvider.Sut.CloudflareWorkerProxied); + Assert.True(sutProvider.Sut.IsBot); + Assert.True(sutProvider.Sut.MaybeBot); + Assert.Equal(botScore, sutProvider.Sut.BotScore); + } + + [Theory, BitAutoData] + public async Task BuildAsync_HttpContext_SetsClientVersion( + SutProvider sutProvider) + { + var httpContext = new DefaultHttpContext(); + var globalSettings = new Core.Settings.GlobalSettings(); + // Arrange + var version = "2024.1.0"; + httpContext.Request.Headers["Bitwarden-Client-Version"] = version; + httpContext.Request.Headers["Is-Prerelease"] = "1"; + + // Act + await sutProvider.Sut.BuildAsync(httpContext, globalSettings); + + // Assert + Assert.Equal(new Version(version), sutProvider.Sut.ClientVersion); + Assert.True(sutProvider.Sut.ClientVersionIsPrerelease); + } + + #endregion + + #region SetContextAsync Tests + + [Theory, BitAutoData] + public async Task SetContextAsync_NullUser_DoesNotThrow( + SutProvider sutProvider) + { + // Act & Assert + await sutProvider.Sut.SetContextAsync(null); + // Should not throw + } + + [Theory, BitAutoData] + public async Task SetContextAsync_UserWithNoClaims_DoesNotThrow( + SutProvider sutProvider) + { + // Arrange + var user = new ClaimsPrincipal(); + + // Act & Assert + await sutProvider.Sut.SetContextAsync(user); + // Should not throw + } + + [Theory, BitAutoData] + public async Task SetContextAsync_SendClient_ShortCircuits( + SutProvider sutProvider, + Guid userId) + { + // Arrange + sutProvider.Sut.UserId = null; + var claims = new List + { + new(Claims.Type, IdentityClientType.Send.ToString()), + new("sub", userId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(IdentityClientType.Send, sutProvider.Sut.IdentityClientType); + Assert.Null(sutProvider.Sut.UserId); // Should not be set for Send clients + } + + [Theory, BitAutoData] + public async Task SetContextAsync_RegularUser_SetsUserId( + SutProvider sutProvider, + Guid userId, + string clientId) + { + // Arrange + var claims = new List + { + new("sub", userId.ToString()), + new("client_id", clientId) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(userId, sutProvider.Sut.UserId); + Assert.Equal(clientId, sutProvider.Sut.ClientId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_InstallationClient_SetsInstallationId( + SutProvider sutProvider, + Guid installationId) + { + // Arrange + var claims = new List + { + new("client_id", "installation.12345"), + new("client_sub", installationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(installationId, sutProvider.Sut.InstallationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_OrganizationClient_SetsOrganizationId( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + var claims = new List + { + new("client_id", "organization.12345"), + new("client_sub", organizationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(organizationId, sutProvider.Sut.OrganizationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_ServiceAccount_SetsServiceAccountOrganizationId( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + var claims = new List + { + new(Claims.Type, IdentityClientType.ServiceAccount.ToString()), + new(Claims.Organization, organizationId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(IdentityClientType.ServiceAccount, sutProvider.Sut.IdentityClientType); + Assert.Equal(organizationId, sutProvider.Sut.ServiceAccountOrganizationId); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_WithDeviceClaims_SetsDeviceInfo( + SutProvider sutProvider, + string deviceIdentifier) + { + // Arrange + var claims = new List + { + new(Claims.Device, deviceIdentifier), + new(Claims.DeviceType, ((int)DeviceType.iOS).ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(deviceIdentifier, sutProvider.Sut.DeviceIdentifier); + Assert.Equal(DeviceType.iOS, sutProvider.Sut.DeviceType); + } + + #endregion + + #region Organization Claims Tests + + [Theory] + [BitAutoData(Claims.OrganizationOwner, OrganizationUserType.Owner)] + [BitAutoData(Claims.OrganizationAdmin, OrganizationUserType.Admin)] + [BitAutoData(Claims.OrganizationUser, OrganizationUserType.User)] + public async Task SetContextAsync_OrganizationClaims_SetsOrganizations( + string userOrgAssociation, + OrganizationUserType userType, + SutProvider sutProvider, + Guid org1Id, + Guid org2Id) + { + // Arrange + var claims = new List + { + new(userOrgAssociation, org1Id.ToString()), + new(userOrgAssociation, org2Id.ToString()), + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Equal(2, sutProvider.Sut.Organizations.Count); + Assert.All(sutProvider.Sut.Organizations, org => Assert.Equal(userType, org.Type)); + Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org1Id); + Assert.Contains(sutProvider.Sut.Organizations, org => org.Id == org2Id); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_OrganizationCustomClaims_SetsOrganizationsWithPermissions( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var claims = new List + { + new(Claims.OrganizationCustom, orgId.ToString()), + new("accesseventlogs", orgId.ToString()), + new("manageusers", orgId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Organizations); + var org = sutProvider.Sut.Organizations.First(); + Assert.Equal(OrganizationUserType.Custom, org.Type); + Assert.Equal(orgId, org.Id); + Assert.True(org.Permissions.AccessEventLogs); + Assert.True(org.Permissions.ManageUsers); + Assert.False(org.Permissions.ManageGroups); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_SecretsManagerAccess_SetsAccessSecretsManager( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var claims = new List + { + new(Claims.OrganizationOwner, orgId.ToString()), + new(Claims.SecretsManagerAccess, orgId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Organizations); + Assert.True(sutProvider.Sut.Organizations.First().AccessSecretsManager); + } + + #endregion + + #region Provider Claims Tests + + [Theory, BitAutoData] + public async Task SetContextAsync_ProviderAdminClaims_SetsProviders( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + var claims = new List + { + new(Claims.ProviderAdmin, providerId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Providers); + Assert.Equal(ProviderUserType.ProviderAdmin, sutProvider.Sut.Providers.First().Type); + Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id); + } + + [Theory, BitAutoData] + public async Task SetContextAsync_ProviderServiceUserClaims_SetsProviders( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + var claims = new List + { + new(Claims.ProviderServiceUser, providerId.ToString()) + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims)); + + // Act + await sutProvider.Sut.SetContextAsync(user); + + // Assert + Assert.Single(sutProvider.Sut.Providers); + Assert.Equal(ProviderUserType.ServiceUser, sutProvider.Sut.Providers.First().Type); + Assert.Equal(providerId, sutProvider.Sut.Providers.First().Id); + } + + #endregion + + #region Organization Permission Tests + + [Theory, BitAutoData] + public async Task OrganizationUser_WithDirectAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.User } + }; + + // Act + var result = await sutProvider.Sut.OrganizationUser(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationUser_WithoutAccess_ReturnsFalse( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List(); + + // Act + var result = await sutProvider.Sut.OrganizationUser(orgId); + + // Assert + Assert.False(result); + } + + [Theory, BitAutoData] + public async Task OrganizationAdmin_WithAdminAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Admin } + }; + + // Act + var result = await sutProvider.Sut.OrganizationAdmin(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationOwner_WithOwnerAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Owner } + }; + + // Act + var result = await sutProvider.Sut.OrganizationOwner(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task OrganizationCustom_WithCustomAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, Type = OrganizationUserType.Custom } + }; + + // Act + var result = await sutProvider.Sut.OrganizationCustom(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public async Task AccessEventLogs_WithPermission_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() + { + Id = orgId, + Type = OrganizationUserType.Custom, + Permissions = new Permissions { AccessEventLogs = true } + } + }; + + // Act + var result = await sutProvider.Sut.AccessEventLogs(orgId); + + // Assert + Assert.True(result); + } + + #endregion + + #region Provider Permission Tests + + [Theory, BitAutoData] + public void ProviderProviderAdmin_WithAdminAccess_ReturnsTrue( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.Sut.Providers = new List + { + new() { Id = providerId, Type = ProviderUserType.ProviderAdmin } + }; + + // Act + var result = sutProvider.Sut.ProviderProviderAdmin(providerId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void ProviderUser_WithAnyAccess_ReturnsTrue( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.Sut.Providers = new List + { + new() { Id = providerId, Type = ProviderUserType.ServiceUser } + }; + + // Act + var result = sutProvider.Sut.ProviderUser(providerId); + + // Assert + Assert.True(result); + } + + #endregion + + #region Secrets Manager Tests + + [Theory, BitAutoData] + public void AccessSecretsManager_WithServiceAccount_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.ServiceAccountOrganizationId = orgId; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void AccessSecretsManager_WithOrgAccess_ReturnsTrue( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, AccessSecretsManager = true } + }; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.True(result); + } + + [Theory, BitAutoData] + public void AccessSecretsManager_WithoutAccess_ReturnsFalse( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List + { + new() { Id = orgId, AccessSecretsManager = false } + }; + + // Act + var result = sutProvider.Sut.AccessSecretsManager(orgId); + + // Assert + Assert.False(result); + } + + #endregion + + #region Membership Loading Tests + + [Theory, BitAutoData] + public async Task OrganizationMembershipAsync_LoadsFromRepository( + SutProvider sutProvider, + Guid userId, + List userOrgs) + { + // Arrange + sutProvider.Sut.UserId = userId; + sutProvider.Sut.Organizations = null; + var organizationUserRepository = Substitute.For(); + userOrgs.ForEach(org => org.Status = OrganizationUserStatusType.Confirmed); + + // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test. + userOrgs.ForEach(org => org.Permissions = "{}"); + organizationUserRepository.GetManyDetailsByUserAsync(userId) + .Returns(userOrgs); + + // Act + var result = await sutProvider.Sut.OrganizationMembershipAsync(organizationUserRepository, userId); + + // Assert + Assert.Equal(userOrgs.Count, result.Count); + Assert.Equal(userId, sutProvider.Sut.UserId); + await organizationUserRepository.Received(1).GetManyDetailsByUserAsync(userId); + } + + [Theory, BitAutoData] + public async Task ProviderMembershipAsync_LoadsFromRepository( + SutProvider sutProvider, + Guid userId, + List userProviders) + { + // Arrange + sutProvider.Sut.UserId = userId; + sutProvider.Sut.Providers = null; + + var providerUserRepository = Substitute.For(); + userProviders.ForEach(provider => provider.Status = ProviderUserStatusType.Confirmed); + + // Test complains about the JSON object that we store permissions as, so just set to empty object to pass the test. + userProviders.ForEach(provider => provider.Permissions = "{}"); + providerUserRepository.GetManyByUserAsync(userId) + .Returns(userProviders); + + // Act + var result = await sutProvider.Sut.ProviderMembershipAsync(providerUserRepository, userId); + + // Assert + Assert.Equal(userProviders.Count, result.Count); + Assert.Equal(userId, sutProvider.Sut.UserId); + await providerUserRepository.Received(1).GetManyByUserAsync(userId); + } + + #endregion + + #region Utility Tests + + [Theory, BitAutoData] + public void GetOrganization_WithExistingOrg_ReturnsOrganization( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + var org = new CurrentContextOrganization { Id = orgId }; + sutProvider.Sut.Organizations = new List { org }; + + // Act + var result = sutProvider.Sut.GetOrganization(orgId); + + // Assert + Assert.Equal(org, result); + } + + [Theory, BitAutoData] + public void GetOrganization_WithNonExistingOrg_ReturnsNull( + SutProvider sutProvider, + Guid orgId) + { + // Arrange + sutProvider.Sut.Organizations = new List(); + + // Act + var result = sutProvider.Sut.GetOrganization(orgId); + + // Assert + Assert.Null(result); + } + + #endregion +} From 18aed0bd798c20abf82c64b5e17a94e483e6d23c Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Fri, 12 Sep 2025 10:41:53 -0500 Subject: [PATCH 233/326] Added conditional subject and button text to invite email. (#6304) * Added conditional subject and button text to invite email. * Added feature flag. --- ...uthorizationHandlerCollectionExtensions.cs | 8 ++-- .../SendOrganizationInvitesCommand.cs | 6 ++- src/Core/Constants.cs | 1 + .../OrganizationUserInvited.html.hbs | 2 +- .../Models/Mail/OrganizationInvitesInfo.cs | 5 +++ .../Mail/OrganizationUserInvitedViewModel.cs | 42 +++++++++++++++++++ .../Implementations/HandlebarsMailService.cs | 38 +++++++++++++---- 7 files changed, 88 insertions(+), 14 deletions(-) diff --git a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs index 70cbc0d1a4..ed628105e0 100644 --- a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs +++ b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs @@ -13,9 +13,9 @@ public static class AuthorizationHandlerCollectionExtensions services.TryAddEnumerable([ ServiceDescriptor.Scoped(), - ServiceDescriptor.Scoped(), - ServiceDescriptor.Scoped(), - ServiceDescriptor.Scoped(), - ]); + ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), + ServiceDescriptor.Scoped(), + ]); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs index cd5066d11b..69b968d438 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs @@ -22,7 +22,8 @@ public class SendOrganizationInvitesCommand( IPolicyRepository policyRepository, IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory dataProtectorTokenFactory, - IMailService mailService) : ISendOrganizationInvitesCommand + IMailService mailService, + IFeatureService featureService) : ISendOrganizationInvitesCommand { public async Task SendInvitesAsync(SendInvitesRequest request) { @@ -71,12 +72,15 @@ public class SendOrganizationInvitesCommand( var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair); + var isSubjectFeatureEnabled = featureService.IsEnabled(FeatureFlagKeys.InviteEmailImprovements); + return new OrganizationInvitesInfo( organization, orgSsoEnabled, orgSsoLoginRequiredPolicyEnabled, orgUsersWithExpTokens, orgUserHasExistingUserDict, + isSubjectFeatureEnabled, initOrganization ); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index bef947b2b7..9f16d12950 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -134,6 +134,7 @@ public static class FeatureFlagKeys public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; + public const string InviteEmailImprovements = "pm-25644-update-join-organization-subject-line"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs index 33c3a9256d..f2594a4c12 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs @@ -3,7 +3,7 @@ - Join Organization Now + {{JoinOrganizationButtonText}} diff --git a/src/Core/Models/Mail/OrganizationInvitesInfo.cs b/src/Core/Models/Mail/OrganizationInvitesInfo.cs index d1c05605e5..c31e00c184 100644 --- a/src/Core/Models/Mail/OrganizationInvitesInfo.cs +++ b/src/Core/Models/Mail/OrganizationInvitesInfo.cs @@ -15,6 +15,7 @@ public class OrganizationInvitesInfo bool orgSsoLoginRequiredPolicyEnabled, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs, Dictionary orgUserHasExistingUserDict, + bool isSubjectFeatureEnabled = false, bool initOrganization = false ) { @@ -29,6 +30,8 @@ public class OrganizationInvitesInfo OrgUserTokenPairs = orgUserTokenPairs; OrgUserHasExistingUserDict = orgUserHasExistingUserDict; + + IsSubjectFeatureEnabled = isSubjectFeatureEnabled; } public string OrganizationName { get; } @@ -38,6 +41,8 @@ public class OrganizationInvitesInfo public string OrgSsoIdentifier { get; } public bool OrgSsoLoginRequiredPolicyEnabled { get; } + public bool IsSubjectFeatureEnabled { get; } + public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; } public Dictionary OrgUserHasExistingUserDict { get; } diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index 82f05af9bd..e43d5a72bd 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -22,6 +22,7 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel GlobalSettings globalSettings) { var freeOrgTitle = "A Bitwarden member invited you to an organization. Join now to start securing your passwords!"; + return new OrganizationUserInvitedViewModel { TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : "Join ", @@ -48,6 +49,45 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel }; } + public static OrganizationUserInvitedViewModel CreateFromInviteInfo_v2( + OrganizationInvitesInfo orgInvitesInfo, + OrganizationUser orgUser, + ExpiringToken expiringToken, + GlobalSettings globalSettings) + { + const string freeOrgTitle = "A Bitwarden member invited you to an organization. " + + "Join now to start securing your passwords!"; + + var userHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id]; + + return new OrganizationUserInvitedViewModel + { + TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : "Join ", + TitleSecondBold = + orgInvitesInfo.IsFreeOrg + ? string.Empty + : CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), + TitleThird = orgInvitesInfo.IsFreeOrg ? string.Empty : " on Bitwarden and start securing your passwords!", + OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), + Email = WebUtility.UrlEncode(orgUser.Email), + OrganizationId = orgUser.OrganizationId.ToString(), + OrganizationUserId = orgUser.Id.ToString(), + Token = WebUtility.UrlEncode(expiringToken.Token), + ExpirationDate = + $"{expiringToken.ExpirationDate.ToLongDateString()} {expiringToken.ExpirationDate.ToShortTimeString()} UTC", + OrganizationNameUrlEncoded = WebUtility.UrlEncode(orgInvitesInfo.OrganizationName), + WebVaultUrl = globalSettings.BaseServiceUri.VaultWithHash, + SiteName = globalSettings.SiteName, + InitOrganization = orgInvitesInfo.InitOrganization, + OrgSsoIdentifier = orgInvitesInfo.OrgSsoIdentifier, + OrgSsoEnabled = orgInvitesInfo.OrgSsoEnabled, + OrgSsoLoginRequiredPolicyEnabled = orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled, + OrgUserHasExistingUser = userHasExistingUser, + JoinOrganizationButtonText = userHasExistingUser || orgInvitesInfo.IsFreeOrg ? "Accept invitation" : "Finish account setup", + IsFreeOrg = orgInvitesInfo.IsFreeOrg + }; + } + public string OrganizationName { get; set; } public string OrganizationId { get; set; } public string OrganizationUserId { get; set; } @@ -60,6 +100,8 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel public bool OrgSsoEnabled { get; set; } public bool OrgSsoLoginRequiredPolicyEnabled { get; set; } public bool OrgUserHasExistingUser { get; set; } + public string JoinOrganizationButtonText { get; set; } = "Join Organization"; + public bool IsFreeOrg { get; set; } public string Url { diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 0410bad19e..89a613b7ed 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -351,21 +351,43 @@ public class HandlebarsMailService : IMailService public async Task SendOrganizationInviteEmailsAsync(OrganizationInvitesInfo orgInvitesInfo) { - MailQueueMessage CreateMessage(string email, object model) - { - var message = CreateDefaultMessage($"Join {orgInvitesInfo.OrganizationName}", email); - return new MailQueueMessage(message, "OrganizationUserInvited", model); - } - var messageModels = orgInvitesInfo.OrgUserTokenPairs.Select(orgUserTokenPair => { Debug.Assert(orgUserTokenPair.OrgUser.Email is not null); - var orgUserInviteViewModel = OrganizationUserInvitedViewModel.CreateFromInviteInfo( - orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token, _globalSettings); + + var orgUserInviteViewModel = orgInvitesInfo.IsSubjectFeatureEnabled + ? OrganizationUserInvitedViewModel.CreateFromInviteInfo_v2( + orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token, _globalSettings) + : OrganizationUserInvitedViewModel.CreateFromInviteInfo(orgInvitesInfo, orgUserTokenPair.OrgUser, + orgUserTokenPair.Token, _globalSettings); + return CreateMessage(orgUserTokenPair.OrgUser.Email, orgUserInviteViewModel); }); await EnqueueMailAsync(messageModels); + return; + + MailQueueMessage CreateMessage(string email, OrganizationUserInvitedViewModel model) + { + var subject = $"Join {model.OrganizationName}"; + + if (orgInvitesInfo.IsSubjectFeatureEnabled) + { + ArgumentNullException.ThrowIfNull(model); + + subject = model! switch + { + { IsFreeOrg: true, OrgUserHasExistingUser: true } => "You have been invited to a Bitwarden Organization", + { IsFreeOrg: true, OrgUserHasExistingUser: false } => "You have been invited to Bitwarden Password Manager", + { IsFreeOrg: false, OrgUserHasExistingUser: true } => $"{model.OrganizationName} invited you to their Bitwarden organization", + { IsFreeOrg: false, OrgUserHasExistingUser: false } => $"{model.OrganizationName} set up a Bitwarden account for you" + }; + } + + var message = CreateDefaultMessage(subject, email); + + return new MailQueueMessage(message, "OrganizationUserInvited", model); + } } public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) From 4e64d35f89bc9aa9b3c6630aa6228ae4d79d4de2 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Fri, 12 Sep 2025 13:24:30 -0400 Subject: [PATCH 234/326] [PM-19151] [PM-19161] Innovation/archive/server (#5672) * Added the ArchivedDate to cipher entity and response model * Created migration scripts for sqlserver and ef core migration to add the ArchivedDate column --------- Co-authored-by: gbubemismith Co-authored-by: SmithThe4th Co-authored-by: Shane Co-authored-by: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Co-authored-by: jng --- .../Vault/Controllers/CiphersController.cs | 89 + .../Models/Request/CipherRequestModel.cs | 14 + .../Models/Response/CipherResponseModel.cs | 2 + src/Core/Constants.cs | 3 + .../Vault/Commands/ArchiveCiphersCommand.cs | 61 + .../Interfaces/IArchiveCiphersCommand.cs | 14 + .../Interfaces/IUnarchiveCiphersCommand.cs | 14 + .../Vault/Commands/UnarchiveCiphersCommand.cs | 60 + src/Core/Vault/Entities/Cipher.cs | 1 + src/Core/Vault/Enums/CipherStateAction.cs | 2 + .../Vault/Repositories/ICipherRepository.cs | 2 + .../Services/Implementations/CipherService.cs | 13 +- .../Vault/VaultServiceCollectionExtensions.cs | 2 + .../Vault/Repositories/CipherRepository.cs | 28 +- .../Queries/UserCipherDetailsQuery.cs | 9 +- .../Vault/Repositories/CipherRepository.cs | 77 +- .../Queries/CipherDetailsQuery.cs | 1 + src/Sql/dbo/Vault/Functions/CipherDetails.sql | 5 +- .../Cipher/CipherDetails_Create.sql | 9 +- .../CipherDetails_CreateWithCollections.sql | 5 +- .../Cipher/CipherDetails_ReadByIdUserId.sql | 4 +- .../Cipher/CipherDetails_Update.sql | 6 +- .../Cipher/Cipher_Archive.sql | 39 + .../Cipher/Cipher_Create.sql | 9 +- .../Cipher/Cipher_CreateWithCollections.sql | 7 +- .../Cipher/Cipher_Unarchive.sql | 39 + .../Cipher/Cipher_Update.sql | 6 +- .../Cipher/Cipher_UpdateWithCollections.sql | 8 +- src/Sql/dbo/Vault/Tables/Cipher.sql | 2 + .../Vault/AutoFixture/CipherFixtures.cs | 4 + .../Commands/ArchiveCiphersCommandTest.cs | 49 + .../Commands/UnarchiveCiphersCommandTest.cs | 49 + .../Repositories/CipherRepositoryTests.cs | 30 + .../2025-09-09_00_CipherArchiveInit.sql | 576 ++++ ...152208_AddArchivedDateToCipher.Designer.cs | 3020 ++++++++++++++++ .../20250829152208_AddArchivedDateToCipher.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...152204_AddArchivedDateToCipher.Designer.cs | 3026 +++++++++++++++++ .../20250829152204_AddArchivedDateToCipher.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + ...152213_AddArchivedDateToCipher.Designer.cs | 3009 ++++++++++++++++ .../20250829152213_AddArchivedDateToCipher.cs | 27 + .../DatabaseContextModelSnapshot.cs | 3 + 43 files changed, 10342 insertions(+), 42 deletions(-) create mode 100644 src/Core/Vault/Commands/ArchiveCiphersCommand.cs create mode 100644 src/Core/Vault/Commands/Interfaces/IArchiveCiphersCommand.cs create mode 100644 src/Core/Vault/Commands/Interfaces/IUnarchiveCiphersCommand.cs create mode 100644 src/Core/Vault/Commands/UnarchiveCiphersCommand.cs create mode 100644 src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql create mode 100644 src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql create mode 100644 test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs create mode 100644 test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs create mode 100644 util/Migrator/DbScripts/2025-09-09_00_CipherArchiveInit.sql create mode 100644 util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.cs create mode 100644 util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.cs create mode 100644 util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.cs diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 84e0488e5a..db3d5fb357 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -20,6 +20,7 @@ using Bit.Core.Settings; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault.Authorization.Permissions; +using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Queries; @@ -48,6 +49,8 @@ public class CiphersController : Controller private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly IApplicationCacheService _applicationCacheService; private readonly ICollectionRepository _collectionRepository; + private readonly IArchiveCiphersCommand _archiveCiphersCommand; + private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand; private readonly IFeatureService _featureService; public CiphersController( @@ -63,6 +66,8 @@ public class CiphersController : Controller IOrganizationCiphersQuery organizationCiphersQuery, IApplicationCacheService applicationCacheService, ICollectionRepository collectionRepository, + IArchiveCiphersCommand archiveCiphersCommand, + IUnarchiveCiphersCommand unarchiveCiphersCommand, IFeatureService featureService) { _cipherRepository = cipherRepository; @@ -77,6 +82,8 @@ public class CiphersController : Controller _organizationCiphersQuery = organizationCiphersQuery; _applicationCacheService = applicationCacheService; _collectionRepository = collectionRepository; + _archiveCiphersCommand = archiveCiphersCommand; + _unarchiveCiphersCommand = unarchiveCiphersCommand; _featureService = featureService; } @@ -846,6 +853,47 @@ public class CiphersController : Controller } } + [HttpPut("{id}/archive")] + [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] + public async Task PutArchive(Guid id) + { + var userId = _userService.GetProperUserId(User).Value; + + var archivedCipherOrganizationDetails = await _archiveCiphersCommand.ArchiveManyAsync([id], userId); + + if (archivedCipherOrganizationDetails.Count == 0) + { + throw new BadRequestException("Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it."); + } + + return new CipherMiniResponseModel(archivedCipherOrganizationDetails.First(), _globalSettings, archivedCipherOrganizationDetails.First().OrganizationUseTotp); + } + + [HttpPut("archive")] + [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] + public async Task> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model) + { + if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) + { + throw new BadRequestException("You can only archive up to 500 items at a time."); + } + + var userId = _userService.GetProperUserId(User).Value; + + var cipherIdsToArchive = new HashSet(model.Ids); + + var archivedCiphers = await _archiveCiphersCommand.ArchiveManyAsync(cipherIdsToArchive, userId); + + if (archivedCiphers.Count == 0) + { + throw new BadRequestException("No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them."); + } + + var responses = archivedCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + + return new ListResponseModel(responses); + } + [HttpDelete("{id}")] [HttpPost("{id}/delete")] public async Task Delete(Guid id) @@ -979,6 +1027,47 @@ public class CiphersController : Controller await _cipherService.SoftDeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true); } + [HttpPut("{id}/unarchive")] + [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] + public async Task PutUnarchive(Guid id) + { + var userId = _userService.GetProperUserId(User).Value; + + var unarchivedCipherDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync([id], userId); + + if (unarchivedCipherDetails.Count == 0) + { + throw new BadRequestException("Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it."); + } + + return new CipherMiniResponseModel(unarchivedCipherDetails.First(), _globalSettings, unarchivedCipherDetails.First().OrganizationUseTotp); + } + + [HttpPut("unarchive")] + [RequireFeature(FeatureFlagKeys.ArchiveVaultItems)] + public async Task> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model) + { + if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) + { + throw new BadRequestException("You can only unarchive up to 500 items at a time."); + } + + var userId = _userService.GetProperUserId(User).Value; + + var cipherIdsToUnarchive = new HashSet(model.Ids); + + var unarchivedCipherOrganizationDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync(cipherIdsToUnarchive, userId); + + if (unarchivedCipherOrganizationDetails.Count == 0) + { + throw new BadRequestException("Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it."); + } + + var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + + return new ListResponseModel(responses); + } + [HttpPut("{id}/restore")] public async Task PutRestore(Guid id) { diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 187fd13e30..467be6e356 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -46,6 +46,7 @@ public class CipherRequestModel public CipherSecureNoteModel SecureNote { get; set; } public CipherSSHKeyModel SSHKey { get; set; } public DateTime? LastKnownRevisionDate { get; set; } = null; + public DateTime? ArchivedDate { get; set; } public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true) { @@ -99,6 +100,7 @@ public class CipherRequestModel existingCipher.Reprompt = Reprompt; existingCipher.Key = Key; + existingCipher.ArchivedDate = ArchivedDate; var hasAttachments2 = (Attachments2?.Count ?? 0) > 0; var hasAttachments = (Attachments?.Count ?? 0) > 0; @@ -316,6 +318,12 @@ public class CipherCollectionsRequestModel public IEnumerable CollectionIds { get; set; } } +public class CipherBulkArchiveRequestModel +{ + [Required] + public IEnumerable Ids { get; set; } +} + public class CipherBulkDeleteRequestModel { [Required] @@ -323,6 +331,12 @@ public class CipherBulkDeleteRequestModel public string OrganizationId { get; set; } } +public class CipherBulkUnarchiveRequestModel +{ + [Required] + public IEnumerable Ids { get; set; } +} + public class CipherBulkRestoreRequestModel { [Required] diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 9d053f6697..3e4e8da512 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -74,6 +74,7 @@ public class CipherMiniResponseModel : ResponseModel DeletedDate = cipher.DeletedDate; Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None); Key = cipher.Key; + ArchivedDate = cipher.ArchivedDate; } public Guid Id { get; set; } @@ -96,6 +97,7 @@ public class CipherMiniResponseModel : ResponseModel public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } public string Key { get; set; } + public DateTime? ArchivedDate { get; set; } } public class CipherResponseModel : CipherMiniResponseModel diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9f16d12950..ed9ee02dad 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -229,6 +229,9 @@ public static class FeatureFlagKeys public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp"; public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption"; + /* Innovation Team */ + public const string ArchiveVaultItems = "pm-19148-innovation-archive"; + public static List GetAllKeys() { return typeof(FeatureFlagKeys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) diff --git a/src/Core/Vault/Commands/ArchiveCiphersCommand.cs b/src/Core/Vault/Commands/ArchiveCiphersCommand.cs new file mode 100644 index 0000000000..6c8e0fcf75 --- /dev/null +++ b/src/Core/Vault/Commands/ArchiveCiphersCommand.cs @@ -0,0 +1,61 @@ +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Vault.Commands; + +public class ArchiveCiphersCommand : IArchiveCiphersCommand +{ + private readonly ICipherRepository _cipherRepository; + private readonly IPushNotificationService _pushService; + + public ArchiveCiphersCommand( + ICipherRepository cipherRepository, + IPushNotificationService pushService + ) + { + _cipherRepository = cipherRepository; + _pushService = pushService; + } + + public async Task> ArchiveManyAsync(IEnumerable cipherIds, + Guid archivingUserId) + { + var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray(); + if (cipherIds == null || cipherIdEnumerable.Length == 0) + throw new BadRequestException("No cipher ids provided."); + + var cipherIdsSet = new HashSet(cipherIdEnumerable); + + var ciphers = await _cipherRepository.GetManyByUserIdAsync(archivingUserId); + + if (ciphers == null || ciphers.Count == 0) + { + return []; + } + + var archivingCiphers = ciphers + .Where(c => cipherIdsSet.Contains(c.Id) && c is { Edit: true, OrganizationId: null, ArchivedDate: null }) + .ToList(); + + var revisionDate = await _cipherRepository.ArchiveAsync(archivingCiphers.Select(c => c.Id), archivingUserId); + + // Adding specifyKind because revisionDate is currently coming back as Unspecified from the database + revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc); + + archivingCiphers.ForEach(c => + { + c.RevisionDate = revisionDate; + c.ArchivedDate = revisionDate; + }); + + // Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers. + // Add event logging here if this is expanded to organization ciphers in the future. + + await _pushService.PushSyncCiphersAsync(archivingUserId); + + return archivingCiphers; + } +} diff --git a/src/Core/Vault/Commands/Interfaces/IArchiveCiphersCommand.cs b/src/Core/Vault/Commands/Interfaces/IArchiveCiphersCommand.cs new file mode 100644 index 0000000000..63df62f160 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/IArchiveCiphersCommand.cs @@ -0,0 +1,14 @@ +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface IArchiveCiphersCommand +{ + /// + /// Archives a cipher. This fills in the ArchivedDate property on a Cipher. + /// + /// Cipher ID to archive. + /// User ID to check against the Ciphers that are trying to be archived. + /// + public Task> ArchiveManyAsync(IEnumerable cipherIds, Guid archivingUserId); +} diff --git a/src/Core/Vault/Commands/Interfaces/IUnarchiveCiphersCommand.cs b/src/Core/Vault/Commands/Interfaces/IUnarchiveCiphersCommand.cs new file mode 100644 index 0000000000..4ed683c0a2 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/IUnarchiveCiphersCommand.cs @@ -0,0 +1,14 @@ +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface IUnarchiveCiphersCommand +{ + /// + /// Unarchives a cipher. This nulls the ArchivedDate property on a Cipher. + /// + /// Cipher ID to unarchive. + /// User ID to check against the Ciphers that are trying to be unarchived. + /// + public Task> UnarchiveManyAsync(IEnumerable cipherIds, Guid unarchivingUserId); +} diff --git a/src/Core/Vault/Commands/UnarchiveCiphersCommand.cs b/src/Core/Vault/Commands/UnarchiveCiphersCommand.cs new file mode 100644 index 0000000000..83dcbab4e1 --- /dev/null +++ b/src/Core/Vault/Commands/UnarchiveCiphersCommand.cs @@ -0,0 +1,60 @@ +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Vault.Commands; + +public class UnarchiveCiphersCommand : IUnarchiveCiphersCommand +{ + private readonly ICipherRepository _cipherRepository; + private readonly IPushNotificationService _pushService; + + public UnarchiveCiphersCommand( + ICipherRepository cipherRepository, + IPushNotificationService pushService + ) + { + _cipherRepository = cipherRepository; + _pushService = pushService; + } + + public async Task> UnarchiveManyAsync(IEnumerable cipherIds, + Guid unarchivingUserId) + { + var cipherIdEnumerable = cipherIds as Guid[] ?? cipherIds.ToArray(); + if (cipherIds == null || cipherIdEnumerable.Length == 0) + throw new BadRequestException("No cipher ids provided."); + + var cipherIdsSet = new HashSet(cipherIdEnumerable); + + var ciphers = await _cipherRepository.GetManyByUserIdAsync(unarchivingUserId); + + if (ciphers == null || ciphers.Count == 0) + { + return []; + } + + var unarchivingCiphers = ciphers + .Where(c => cipherIdsSet.Contains(c.Id) && c is { Edit: true, ArchivedDate: not null }) + .ToList(); + + var revisionDate = + await _cipherRepository.UnarchiveAsync(unarchivingCiphers.Select(c => c.Id), unarchivingUserId); + // Adding specifyKind because revisionDate is currently coming back as Unspecified from the database + revisionDate = DateTime.SpecifyKind(revisionDate, DateTimeKind.Utc); + + unarchivingCiphers.ForEach(c => + { + c.RevisionDate = revisionDate; + c.ArchivedDate = null; + }); + // Will not log an event because the archive feature is limited to individual ciphers, and event logs only apply to organization ciphers. + // Add event logging here if this is expanded to organization ciphers in the future. + + await _pushService.PushSyncCiphersAsync(unarchivingUserId); + + return unarchivingCiphers; + } +} diff --git a/src/Core/Vault/Entities/Cipher.cs b/src/Core/Vault/Entities/Cipher.cs index 8d8282d83c..f6afc090bb 100644 --- a/src/Core/Vault/Entities/Cipher.cs +++ b/src/Core/Vault/Entities/Cipher.cs @@ -25,6 +25,7 @@ public class Cipher : ITableObject, ICloneable public DateTime? DeletedDate { get; set; } public Enums.CipherRepromptType? Reprompt { get; set; } public string Key { get; set; } + public DateTime? ArchivedDate { get; set; } public void SetNewId() { diff --git a/src/Core/Vault/Enums/CipherStateAction.cs b/src/Core/Vault/Enums/CipherStateAction.cs index adbc78c06c..d63315e63f 100644 --- a/src/Core/Vault/Enums/CipherStateAction.cs +++ b/src/Core/Vault/Enums/CipherStateAction.cs @@ -3,6 +3,8 @@ public enum CipherStateAction { Restore, + Unarchive, + Archive, SoftDelete, HardDelete, } diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index e442477921..32acf3cbc9 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -25,6 +25,7 @@ public interface ICipherRepository : IRepository Task ReplaceAsync(Cipher obj, IEnumerable collectionIds); Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); Task UpdateAttachmentAsync(CipherAttachment attachment); + Task ArchiveAsync(IEnumerable ids, Guid userId); Task DeleteAttachmentAsync(Guid cipherId, string attachmentId); Task DeleteAsync(IEnumerable ids, Guid userId); Task DeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); @@ -56,6 +57,7 @@ public interface ICipherRepository : IRepository IEnumerable collectionCiphers, IEnumerable collectionUsers); Task SoftDeleteAsync(IEnumerable ids, Guid userId); Task SoftDeleteByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); + Task UnarchiveAsync(IEnumerable ids, Guid userId); Task RestoreAsync(IEnumerable ids, Guid userId); Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task DeleteDeletedAsync(DateTime deletedDateBefore); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index e0b121fdd3..ebfb2a4a2a 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -481,7 +481,7 @@ public class CipherService : ICipherService throw new NotFoundException(); } await _cipherRepository.DeleteByOrganizationIdAsync(organizationId); - await _eventService.LogOrganizationEventAsync(org, Bit.Core.Enums.EventType.Organization_PurgedVault); + await _eventService.LogOrganizationEventAsync(org, EventType.Organization_PurgedVault); } public async Task MoveManyAsync(IEnumerable cipherIds, Guid? destinationFolderId, Guid movingUserId) @@ -697,7 +697,7 @@ public class CipherService : ICipherService await _collectionCipherRepository.UpdateCollectionsAsync(cipher.Id, savingUserId, collectionIds); } - await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_UpdatedCollections); + await _eventService.LogCipherEventAsync(cipher, EventType.Cipher_UpdatedCollections); // push await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); @@ -786,8 +786,8 @@ public class CipherService : ICipherService } var cipherIdsSet = new HashSet(cipherIds); - var restoringCiphers = new List(); - DateTime? revisionDate; + List restoringCiphers; + DateTime? revisionDate; // TODO: Make this not nullable if (orgAdmin && organizationId.HasValue) { @@ -971,6 +971,11 @@ public class CipherService : ICipherService throw new BadRequestException("One or more ciphers do not belong to you."); } + if (cipher.ArchivedDate.HasValue) + { + throw new BadRequestException("Cipher cannot be shared with organization because it is archived."); + } + var attachments = cipher.GetAttachments(); var hasAttachments = attachments?.Any() ?? false; var org = await _organizationRepository.GetByIdAsync(organizationId); diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 1acc74959d..93e86c0208 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -24,6 +24,8 @@ public static class VaultServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 08593191f1..c741495f8e 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -240,11 +240,24 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task ArchiveAsync(IEnumerable ids, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteScalarAsync( + $"[{Schema}].[Cipher_Archive]", + new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results; + } + } + public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId) { using (var connection = new SqlConnection(ConnectionString)) { - var results = await connection.ExecuteAsync( + await connection.ExecuteAsync( $"[{Schema}].[Cipher_DeleteAttachment]", new { Id = cipherId, AttachmentId = attachmentId }, commandType: CommandType.StoredProcedure); @@ -830,6 +843,19 @@ public class CipherRepository : Repository, ICipherRepository } } + public async Task UnarchiveAsync(IEnumerable ids, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteScalarAsync( + $"[{Schema}].[Cipher_Unarchive]", + new { Ids = ids.ToGuidIdArrayTVP(), UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results; + } + } + public async Task RestoreAsync(IEnumerable ids, Guid userId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs index 98d555ff19..b196a07e9b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/UserCipherDetailsQuery.cs @@ -71,7 +71,8 @@ public class UserCipherDetailsQuery : IQuery Manage = cu == null ? (cg != null && cg.Manage == true) : cu.Manage == true, OrganizationUseTotp = o.UseTotp, c.Reprompt, - c.Key + c.Key, + c.ArchivedDate }; var query2 = from c in dbContext.Ciphers @@ -94,7 +95,8 @@ public class UserCipherDetailsQuery : IQuery Manage = true, OrganizationUseTotp = false, c.Reprompt, - c.Key + c.Key, + c.ArchivedDate }; var union = query.Union(query2).Select(c => new CipherDetails @@ -115,7 +117,8 @@ public class UserCipherDetailsQuery : IQuery ViewPassword = c.ViewPassword, Manage = c.Manage, OrganizationUseTotp = c.OrganizationUseTotp, - Key = c.Key + Key = c.Key, + ArchivedDate = c.ArchivedDate }); return union; } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 1a137c5f4b..4b2d09f87b 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -230,7 +230,7 @@ public class CipherRepository : Repository ids, Guid userId) { - await ToggleCipherStates(ids, userId, CipherStateAction.HardDelete); + await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.HardDelete); } public async Task DeleteAttachmentAsync(Guid cipherId, string attachmentId) @@ -508,7 +508,8 @@ public class CipherRepository : Repository UnarchiveAsync(IEnumerable ids, Guid userId) + { + return await ToggleArchiveCipherStatesAsync(ids, userId, CipherStateAction.Unarchive); + } + public async Task RestoreAsync(IEnumerable ids, Guid userId) { - return await ToggleCipherStates(ids, userId, CipherStateAction.Restore); + return await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.Restore); } public async Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId) @@ -781,20 +787,25 @@ public class CipherRepository : Repository ids, Guid userId) + public async Task ArchiveAsync(IEnumerable ids, Guid userId) { - await ToggleCipherStates(ids, userId, CipherStateAction.SoftDelete); + return await ToggleArchiveCipherStatesAsync(ids, userId, CipherStateAction.Archive); } - private async Task ToggleCipherStates(IEnumerable ids, Guid userId, CipherStateAction action) + public async Task SoftDeleteAsync(IEnumerable ids, Guid userId) { - static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd) + await ToggleDeleteCipherStatesAsync(ids, userId, CipherStateAction.SoftDelete); + } + + private async Task ToggleArchiveCipherStatesAsync(IEnumerable ids, Guid userId, CipherStateAction action) + { + static bool FilterArchivedDate(CipherStateAction action, CipherDetails ucd) { return action switch { - CipherStateAction.Restore => ucd.DeletedDate != null, - CipherStateAction.SoftDelete => ucd.DeletedDate == null, - _ => true, + CipherStateAction.Unarchive => ucd.ArchivedDate != null, + CipherStateAction.Archive => ucd.ArchivedDate == null, + _ => true }; } @@ -802,8 +813,49 @@ public class CipherRepository : Repository ids.Contains(c.Id))).ToListAsync(); - var query = from ucd in await (userCipherDetailsQuery.Run(dbContext)).ToListAsync() + var cipherEntitiesToCheck = await dbContext.Ciphers.Where(c => ids.Contains(c.Id)).ToListAsync(); + var query = from ucd in await userCipherDetailsQuery.Run(dbContext).ToListAsync() + join c in cipherEntitiesToCheck + on ucd.Id equals c.Id + where ucd.Edit && FilterArchivedDate(action, ucd) + select c; + + var utcNow = DateTime.UtcNow; + var cipherIdsToModify = query.Select(c => c.Id); + var cipherEntitiesToModify = dbContext.Ciphers.Where(x => cipherIdsToModify.Contains(x.Id)); + + await cipherEntitiesToModify.ForEachAsync(cipher => + { + dbContext.Attach(cipher); + cipher.ArchivedDate = action == CipherStateAction.Unarchive ? null : utcNow; + cipher.RevisionDate = utcNow; + }); + + await dbContext.UserBumpAccountRevisionDateAsync(userId); + await dbContext.SaveChangesAsync(); + + return utcNow; + } + } + + private async Task ToggleDeleteCipherStatesAsync(IEnumerable ids, Guid userId, CipherStateAction action) + { + static bool FilterDeletedDate(CipherStateAction action, CipherDetails ucd) + { + return action switch + { + CipherStateAction.Restore => ucd.DeletedDate != null, + CipherStateAction.SoftDelete => ucd.DeletedDate == null, + _ => true + }; + } + + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var userCipherDetailsQuery = new UserCipherDetailsQuery(userId); + var cipherEntitiesToCheck = await dbContext.Ciphers.Where(c => ids.Contains(c.Id)).ToListAsync(); + var query = from ucd in await userCipherDetailsQuery.Run(dbContext).ToListAsync() join c in cipherEntitiesToCheck on ucd.Id equals c.Id where ucd.Edit && FilterDeletedDate(action, ucd) @@ -841,6 +893,7 @@ public class CipherRepository : Repository FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ? null : CoreHelpers.LoadClassFromJsonData>(c.Folders)[_userId.Value], + ArchivedDate = c.ArchivedDate, }; return query; } diff --git a/src/Sql/dbo/Vault/Functions/CipherDetails.sql b/src/Sql/dbo/Vault/Functions/CipherDetails.sql index 5577ff4787..ed92c11cb6 100644 --- a/src/Sql/dbo/Vault/Functions/CipherDetails.sql +++ b/src/Sql/dbo/Vault/Functions/CipherDetails.sql @@ -27,6 +27,7 @@ SELECT END [FolderId], C.[DeletedDate], C.[Reprompt], - C.[Key] + C.[Key], + C.[ArchivedDate] FROM - [dbo].[Cipher] C \ No newline at end of file + [dbo].[Cipher] C diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql index d0e08fcd08..254110f059 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql @@ -17,7 +17,8 @@ @OrganizationUseTotp BIT, -- not used @DeletedDate DATETIME2(7), @Reprompt TINYINT, - @Key VARCHAR(MAX) = NULL + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -38,7 +39,8 @@ BEGIN [RevisionDate], [DeletedDate], [Reprompt], - [Key] + [Key], + [ArchivedDate] ) VALUES ( @@ -53,7 +55,8 @@ BEGIN @RevisionDate, @DeletedDate, @Reprompt, - @Key + @Key, + @ArchivedDate ) IF @OrganizationId IS NOT NULL diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql index 6e61d3d385..ee7e00b32a 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql @@ -18,14 +18,15 @@ @DeletedDate DATETIME2(7), @Reprompt TINYINT, @Key VARCHAR(MAX) = NULL, - @CollectionIds AS [dbo].[GuidIdArray] READONLY + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword, @Manage, - @OrganizationUseTotp, @DeletedDate, @Reprompt, @Key + @OrganizationUseTotp, @DeletedDate, @Reprompt, @Key, @ArchivedDate DECLARE @UpdateCollectionsSuccess INT EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql index 7e2c893a41..2646159b62 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql @@ -20,6 +20,7 @@ SELECT [Reprompt], [Key], [OrganizationUseTotp], + [ArchivedDate], MAX ([Edit]) AS [Edit], MAX ([ViewPassword]) AS [ViewPassword], MAX ([Manage]) AS [Manage] @@ -41,5 +42,6 @@ SELECT [DeletedDate], [Reprompt], [Key], - [OrganizationUseTotp] + [OrganizationUseTotp], + [ArchivedDate] END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql index 8fc95eb302..c17f5761ff 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql @@ -17,7 +17,8 @@ @OrganizationUseTotp BIT, -- not used @DeletedDate DATETIME2(2), @Reprompt TINYINT, - @Key VARCHAR(MAX) = NULL + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -55,7 +56,8 @@ BEGIN [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [DeletedDate] = @DeletedDate, - [Key] = @Key + [Key] = @Key, + [ArchivedDate] = @ArchivedDate WHERE [Id] = @Id diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql new file mode 100644 index 0000000000..68f11c0d4f --- /dev/null +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Archive.sql @@ -0,0 +1,39 @@ +CREATE PROCEDURE [dbo].[Cipher_Archive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [ArchivedDate] IS NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [ArchivedDate] = @UtcNow, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql index 676c013cc8..eb49136895 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql @@ -11,7 +11,8 @@ @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @Reprompt TINYINT, - @Key VARCHAR(MAX) = NULL + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -29,7 +30,8 @@ BEGIN [RevisionDate], [DeletedDate], [Reprompt], - [Key] + [Key], + [ArchivedDate] ) VALUES ( @@ -44,7 +46,8 @@ BEGIN @RevisionDate, @DeletedDate, @Reprompt, - @Key + @Key, + @ArchivedDate ) IF @OrganizationId IS NOT NULL diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql index 775ab0e0a0..ac7be1bbae 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql @@ -12,14 +12,15 @@ @DeletedDate DATETIME2(7), @Reprompt TINYINT, @Key VARCHAR(MAX) = NULL, - @CollectionIds AS [dbo].[GuidIdArray] READONLY + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, - @Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key + @Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key, @ArchivedDate DECLARE @UpdateCollectionsSuccess INT EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql new file mode 100644 index 0000000000..c2b7b10619 --- /dev/null +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Unarchive.sql @@ -0,0 +1,39 @@ +CREATE PROCEDURE [dbo].[Cipher_Unarchive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [ArchivedDate] IS NOT NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [ArchivedDate] = NULL, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql index 8baf1b5f0f..912badc906 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql @@ -11,7 +11,8 @@ @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @Reprompt TINYINT, - @Key VARCHAR(MAX) = NULL + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -30,7 +31,8 @@ BEGIN [RevisionDate] = @RevisionDate, [DeletedDate] = @DeletedDate, [Reprompt] = @Reprompt, - [Key] = @Key + [Key] = @Key, + [ArchivedDate] = @ArchivedDate WHERE [Id] = @Id diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql index 0a0c980e4a..55852c4d27 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql @@ -12,7 +12,8 @@ @DeletedDate DATETIME2(7), @Reprompt TINYINT, @Key VARCHAR(MAX) = NULL, - @CollectionIds AS [dbo].[GuidIdArray] READONLY + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL AS BEGIN SET NOCOUNT ON @@ -37,8 +38,7 @@ BEGIN [Data] = @Data, [Attachments] = @Attachments, [RevisionDate] = @RevisionDate, - [DeletedDate] = @DeletedDate, - [Key] = @Key + [DeletedDate] = @DeletedDate, [Key] = @Key, [ArchivedDate] = @ArchivedDate -- No need to update CreationDate, Favorites, Folders, or Type since that data will not change WHERE [Id] = @Id @@ -54,4 +54,4 @@ BEGIN EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId SELECT 0 -- 0 = Success -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Vault/Tables/Cipher.sql b/src/Sql/dbo/Vault/Tables/Cipher.sql index 38dd47d21f..d69035a0a9 100644 --- a/src/Sql/dbo/Vault/Tables/Cipher.sql +++ b/src/Sql/dbo/Vault/Tables/Cipher.sql @@ -13,6 +13,7 @@ CREATE TABLE [dbo].[Cipher] ( [DeletedDate] DATETIME2 (7) NULL, [Reprompt] TINYINT NULL, [Key] VARCHAR(MAX) NULL, + [ArchivedDate] DATETIME2 (7) NULL, CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_Cipher_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_Cipher_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) @@ -34,4 +35,5 @@ GO CREATE NONCLUSTERED INDEX [IX_Cipher_DeletedDate] ON [dbo].[Cipher]([DeletedDate] ASC); + GO diff --git a/test/Core.Test/Vault/AutoFixture/CipherFixtures.cs b/test/Core.Test/Vault/AutoFixture/CipherFixtures.cs index d3fe5f7f8e..f2feb82927 100644 --- a/test/Core.Test/Vault/AutoFixture/CipherFixtures.cs +++ b/test/Core.Test/Vault/AutoFixture/CipherFixtures.cs @@ -12,9 +12,11 @@ internal class OrganizationCipher : ICustomization { fixture.Customize(composer => composer .With(c => c.OrganizationId, OrganizationId ?? Guid.NewGuid()) + .Without(c => c.ArchivedDate) .Without(c => c.UserId)); fixture.Customize(composer => composer .With(c => c.OrganizationId, Guid.NewGuid()) + .Without(c => c.ArchivedDate) .Without(c => c.UserId)); } } @@ -26,9 +28,11 @@ internal class UserCipher : ICustomization { fixture.Customize(composer => composer .With(c => c.UserId, UserId ?? Guid.NewGuid()) + .Without(c => c.ArchivedDate) .Without(c => c.OrganizationId)); fixture.Customize(composer => composer .With(c => c.UserId, Guid.NewGuid()) + .Without(c => c.ArchivedDate) .Without(c => c.OrganizationId)); } } diff --git a/test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs b/test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs new file mode 100644 index 0000000000..624db7941d --- /dev/null +++ b/test/Core.Test/Vault/Commands/ArchiveCiphersCommandTest.cs @@ -0,0 +1,49 @@ +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Test.AutoFixture.CipherFixtures; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Commands; + +[UserCipherCustomize] +[SutProviderCustomize] +public class ArchiveCiphersCommandTest +{ + [Theory] + [BitAutoData(true, false, 1, 1, 1)] + [BitAutoData(false, false, 1, 0, 1)] + [BitAutoData(false, true, 1, 0, 1)] + [BitAutoData(true, true, 1, 0, 1)] + public async Task ArchiveAsync_Works( + bool isEditable, bool hasOrganizationId, + int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls, + SutProvider sutProvider, CipherDetails cipher, User user) + { + cipher.Edit = isEditable; + cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null; + + var cipherList = new List { cipher }; + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id).Returns(cipherList); + + // Act + await sutProvider.Sut.ArchiveManyAsync([cipher.Id], user.Id); + + // Assert + await sutProvider.GetDependency().Received(cipherRepoCalls).ArchiveAsync( + Arg.Is>(ids => ids.Count() == resultCountFromQuery + && ids.Count() >= 1 + ? true + : ids.All(id => cipherList.Contains(cipher))), + user.Id); + await sutProvider.GetDependency().Received(pushNotificationsCalls) + .PushSyncCiphersAsync(user.Id); + } +} diff --git a/test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs b/test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs new file mode 100644 index 0000000000..0a41f1cce8 --- /dev/null +++ b/test/Core.Test/Vault/Commands/UnarchiveCiphersCommandTest.cs @@ -0,0 +1,49 @@ +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Test.AutoFixture.CipherFixtures; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Commands; + +[UserCipherCustomize] +[SutProviderCustomize] +public class UnarchiveCiphersCommandTest +{ + [Theory] + [BitAutoData(true, false, 1, 1, 1)] + [BitAutoData(false, false, 1, 0, 1)] + [BitAutoData(false, true, 1, 0, 1)] + [BitAutoData(true, true, 1, 1, 1)] + public async Task UnarchiveAsync_Works( + bool isEditable, bool hasOrganizationId, + int cipherRepoCalls, int resultCountFromQuery, int pushNotificationsCalls, + SutProvider sutProvider, CipherDetails cipher, User user) + { + cipher.Edit = isEditable; + cipher.OrganizationId = hasOrganizationId ? Guid.NewGuid() : null; + + var cipherList = new List { cipher }; + + sutProvider.GetDependency() + .GetManyByUserIdAsync(user.Id).Returns(cipherList); + + // Act + await sutProvider.Sut.UnarchiveManyAsync([cipher.Id], user.Id); + + // Assert + await sutProvider.GetDependency().Received(cipherRepoCalls).UnarchiveAsync( + Arg.Is>(ids => ids.Count() == resultCountFromQuery + && ids.Count() >= 1 + ? true + : ids.All(id => cipherList.Contains(cipher))), + user.Id); + await sutProvider.GetDependency().Received(pushNotificationsCalls) + .PushSyncCiphersAsync(user.Id); + } +} diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 2a31398a02..ef28d776d7 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -1211,4 +1211,34 @@ public class CipherRepositoryTests Assert.Null(deletedCipher2); } + + [DatabaseTheory, DatabaseData] + public async Task ArchiveAsync_Works( + ICipherRepository sutRepository, + IUserRepository userRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + // Ciphers + var cipher = await sutRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + Data = "", + UserId = user.Id + }); + + // Act + await sutRepository.ArchiveAsync(new List { cipher.Id }, user.Id); + + // Assert + var archivedCipher = await sutRepository.GetByIdAsync(cipher.Id, user.Id); + Assert.NotNull(archivedCipher); + Assert.NotNull(archivedCipher.ArchivedDate); + } } diff --git a/util/Migrator/DbScripts/2025-09-09_00_CipherArchiveInit.sql b/util/Migrator/DbScripts/2025-09-09_00_CipherArchiveInit.sql new file mode 100644 index 0000000000..e1ef1a2399 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-09_00_CipherArchiveInit.sql @@ -0,0 +1,576 @@ +-- Add the ArchivedDate column to the Cipher table +IF COL_LENGTH('[dbo].[Cipher]', 'ArchivedDate') IS NULL +BEGIN + ALTER TABLE [dbo].[Cipher] + ADD [ArchivedDate] DATETIME2(7) NULL + END +GO + +-- Recreate CipherView +CREATE OR ALTER VIEW [dbo].[CipherView] +AS +SELECT + * +FROM + [dbo].[Cipher] + GO + +-- Alter CipherDetails function +CREATE OR ALTER FUNCTION [dbo].[CipherDetails](@UserId UNIQUEIDENTIFIER) +RETURNS TABLE +AS RETURN +SELECT + C.[Id], + C.[UserId], + C.[OrganizationId], + C.[Type], + C.[Data], + C.[Attachments], + C.[CreationDate], + C.[RevisionDate], + CASE + WHEN + @UserId IS NULL + OR C.[Favorites] IS NULL + OR JSON_VALUE(C.[Favorites], CONCAT('$."', @UserId, '"')) IS NULL + THEN 0 + ELSE 1 + END [Favorite], + CASE + WHEN + @UserId IS NULL + OR C.[Folders] IS NULL + THEN NULL + ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"'))) +END [FolderId], + C.[DeletedDate], + C.[Reprompt], + C.[Key], + C.[ArchivedDate] +FROM + [dbo].[Cipher] C +GO + + +-- Manually refresh UserCipherDetails +IF OBJECT_ID('[dbo].[UserCipherDetails]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[UserCipherDetails]'; +END +GO + + +-- Update sprocs +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [CreationDate], + [RevisionDate], + [DeletedDate], + [Reprompt], + [Key], + [ArchivedDate] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + @Favorites, + @Folders, + @CreationDate, + @RevisionDate, + @DeletedDate, + @Reprompt, + @Key, + @ArchivedDate + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Favorites] = @Favorites, + [Folders] = @Folders, + [Attachments] = @Attachments, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Reprompt] = @Reprompt, + [Key] = @Key, + [ArchivedDate] = @ArchivedDate + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [CreationDate], + [RevisionDate], + [DeletedDate], + [Reprompt], + [Key], + [ArchivedDate] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + CASE WHEN @Favorite = 1 THEN CONCAT('{', @UserIdKey, ':true}') ELSE NULL END, + CASE WHEN @FolderId IS NOT NULL THEN CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') ELSE NULL END, + @CreationDate, + @RevisionDate, + @DeletedDate, + @Reprompt, + @Key, + @ArchivedDate + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(2), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END, + [Favorites] = + CASE + WHEN @Favorite = 1 AND [Favorites] IS NULL THEN + CONCAT('{', @UserIdKey, ':true}') + WHEN @Favorite = 1 THEN + JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT)) + ELSE + JSON_MODIFY([Favorites], @UserIdPath, NULL) + END, + [Attachments] = @Attachments, + [Reprompt] = @Reprompt, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Key] = @Key, + [ArchivedDate] = @ArchivedDate + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, + @Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword, @Manage, + @OrganizationUseTotp, @DeletedDate, @Reprompt, @Key, @ArchivedDate + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_CreateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders, + @Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key, @ArchivedDate + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_UpdateWithCollections] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL, + @CollectionIds AS [dbo].[GuidIdArray] READONLY, + @ArchivedDate DATETIME2(7) = NULL +AS +BEGIN + SET NOCOUNT ON + +BEGIN TRANSACTION Cipher_UpdateWithCollections + + DECLARE @UpdateCollectionsSuccess INT + EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds + + IF @UpdateCollectionsSuccess < 0 +BEGIN + COMMIT TRANSACTION Cipher_UpdateWithCollections + SELECT -1 -- -1 = Failure + RETURN +END + +UPDATE + [dbo].[Cipher] +SET + [UserId] = NULL, + [OrganizationId] = @OrganizationId, + [Data] = @Data, + [Attachments] = @Attachments, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Key] = @Key, + [ArchivedDate] = @ArchivedDate +-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change +WHERE + [Id] = @Id + + COMMIT TRANSACTION Cipher_UpdateWithCollections + + IF @Attachments IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_UpdateStorage] @UserId + END + + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + + SELECT 0 -- 0 = Success +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Archive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [ArchivedDate] IS NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [ArchivedDate] = @UtcNow, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Unarchive] + @Ids AS [dbo].[GuidIdArray] READONLY, + @UserId AS UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + CREATE TABLE #Temp + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NULL + ) + + INSERT INTO #Temp + SELECT + [Id], + [UserId] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Edit] = 1 + AND [ArchivedDate] IS NOT NULL + AND [Id] IN (SELECT * FROM @Ids) + + DECLARE @UtcNow DATETIME2(7) = SYSUTCDATETIME(); + UPDATE + [dbo].[Cipher] + SET + [ArchivedDate] = NULL, + [RevisionDate] = @UtcNow + WHERE + [Id] IN (SELECT [Id] FROM #Temp) + + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + + DROP TABLE #Temp + + SELECT @UtcNow +END +GO + +-- Update User Cipher Details With Archive + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp], + [ArchivedDate], + MAX ([Edit]) AS [Edit], + MAX ([ViewPassword]) AS [ViewPassword], + MAX ([Manage]) AS [Manage] + FROM + [dbo].[UserCipherDetails](@UserId) + WHERE + [Id] = @Id + GROUP BY + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Attachments], + [CreationDate], + [RevisionDate], + [Favorite], + [FolderId], + [DeletedDate], + [Reprompt], + [Key], + [OrganizationUseTotp], + [ArchivedDate] +END diff --git a/util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.Designer.cs b/util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.Designer.cs new file mode 100644 index 0000000000..a9b8277856 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.Designer.cs @@ -0,0 +1,3020 @@ +// +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("20250829152208_AddArchivedDateToCipher")] + partial class AddArchivedDateToCipher + { + /// + 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.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("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .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("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.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(50) + .HasColumnType("varchar(50)"); + + 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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("LastSyncDate") + .HasColumnType("datetime(6)"); + + 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("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.Tools.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.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.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.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"); + + 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.Tools.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.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"); + + 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/20250829152208_AddArchivedDateToCipher.cs b/util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.cs new file mode 100644 index 0000000000..bd2a159a39 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250829152208_AddArchivedDateToCipher.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddArchivedDateToCipher : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ArchivedDate", + table: "Cipher", + type: "datetime(6)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ArchivedDate", + table: "Cipher"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 69301d7e54..beab31cebf 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2184,6 +2184,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Id") .HasColumnType("char(36)"); + b.Property("ArchivedDate") + .HasColumnType("datetime(6)"); + b.Property("Attachments") .HasColumnType("longtext"); diff --git a/util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.Designer.cs b/util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.Designer.cs new file mode 100644 index 0000000000..97d551603a --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.Designer.cs @@ -0,0 +1,3026 @@ +// +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("20250829152204_AddArchivedDateToCipher")] + partial class AddArchivedDateToCipher + { + /// + 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.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("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .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("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.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(50) + .HasColumnType("character varying(50)") + .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.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("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.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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + 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("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.Tools.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.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.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.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"); + + 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.Tools.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.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"); + + 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/20250829152204_AddArchivedDateToCipher.cs b/util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.cs new file mode 100644 index 0000000000..f52d7601a4 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250829152204_AddArchivedDateToCipher.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddArchivedDateToCipher : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ArchivedDate", + table: "Cipher", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ArchivedDate", + table: "Cipher"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index b0e34084e8..3d66e2c035 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2190,6 +2190,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Id") .HasColumnType("uuid"); + b.Property("ArchivedDate") + .HasColumnType("timestamp with time zone"); + b.Property("Attachments") .HasColumnType("text"); diff --git a/util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.Designer.cs b/util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.Designer.cs new file mode 100644 index 0000000000..23b6c6c752 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.Designer.cs @@ -0,0 +1,3009 @@ +// +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("20250829152213_AddArchivedDateToCipher")] + partial class AddArchivedDateToCipher + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + 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("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .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("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.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(50) + .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.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("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + 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("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("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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("LastSyncDate") + .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("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.Tools.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.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.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.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"); + + 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.Tools.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.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"); + + 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/20250829152213_AddArchivedDateToCipher.cs b/util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.cs new file mode 100644 index 0000000000..38fec3fe81 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250829152213_AddArchivedDateToCipher.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddArchivedDateToCipher : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ArchivedDate", + table: "Cipher", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ArchivedDate", + table: "Cipher"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index caee8fef2a..d091cb4830 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -2173,6 +2173,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Id") .HasColumnType("TEXT"); + b.Property("ArchivedDate") + .HasColumnType("TEXT"); + b.Property("Attachments") .HasColumnType("TEXT"); From 854abb0993ccc6be05b400f269674920adf4586d Mon Sep 17 00:00:00 2001 From: Jimmy Vo Date: Fri, 12 Sep 2025 13:44:19 -0400 Subject: [PATCH 235/326] [PM-23845] Update cache service to handle concurrency (#6170) --- .../IApplicationCacheServiceBusMessaging.cs | 10 + ...VCurrentInMemoryApplicationCacheService.cs | 19 + .../IVNextInMemoryApplicationCacheService.cs | 19 + .../NoOpApplicationCacheMessaging.cs | 21 + .../ServiceBusApplicationCacheMessaging.cs | 63 ++ .../VNextInMemoryApplicationCacheService.cs | 137 +++++ src/Core/Constants.cs | 2 + .../ApplicationCacheHostedService.cs | 5 +- .../FeatureRoutedCacheService.cs | 152 +++++ .../InMemoryApplicationCacheService.cs | 3 +- ...MemoryServiceBusApplicationCacheService.cs | 5 +- src/Events/Startup.cs | 11 +- .../Utilities/ServiceCollectionExtensions.cs | 11 +- ...extInMemoryApplicationCacheServiceTests.cs | 403 +++++++++++++ .../FeatureRoutedCacheServiceTests.cs | 541 ++++++++++++++++++ 15 files changed, 1392 insertions(+), 10 deletions(-) create mode 100644 src/Core/AdminConsole/AbilitiesCache/IApplicationCacheServiceBusMessaging.cs create mode 100644 src/Core/AdminConsole/AbilitiesCache/IVCurrentInMemoryApplicationCacheService.cs create mode 100644 src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs create mode 100644 src/Core/AdminConsole/AbilitiesCache/NoOpApplicationCacheMessaging.cs create mode 100644 src/Core/AdminConsole/AbilitiesCache/ServiceBusApplicationCacheMessaging.cs create mode 100644 src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs create mode 100644 src/Core/Services/Implementations/FeatureRoutedCacheService.cs create mode 100644 test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs create mode 100644 test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs diff --git a/src/Core/AdminConsole/AbilitiesCache/IApplicationCacheServiceBusMessaging.cs b/src/Core/AdminConsole/AbilitiesCache/IApplicationCacheServiceBusMessaging.cs new file mode 100644 index 0000000000..d0cecfb10d --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/IApplicationCacheServiceBusMessaging.cs @@ -0,0 +1,10 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public interface IApplicationCacheServiceBusMessaging +{ + Task NotifyOrganizationAbilityUpsertedAsync(Organization organization); + Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId); + Task NotifyProviderAbilityDeletedAsync(Guid providerId); +} diff --git a/src/Core/AdminConsole/AbilitiesCache/IVCurrentInMemoryApplicationCacheService.cs b/src/Core/AdminConsole/AbilitiesCache/IVCurrentInMemoryApplicationCacheService.cs new file mode 100644 index 0000000000..e8152b1e98 --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/IVCurrentInMemoryApplicationCacheService.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public interface IVCurrentInMemoryApplicationCacheService +{ + Task> GetOrganizationAbilitiesAsync(); +#nullable enable + Task GetOrganizationAbilityAsync(Guid orgId); +#nullable disable + Task> GetProviderAbilitiesAsync(); + Task UpsertOrganizationAbilityAsync(Organization organization); + Task UpsertProviderAbilityAsync(Provider provider); + Task DeleteOrganizationAbilityAsync(Guid organizationId); + Task DeleteProviderAbilityAsync(Guid providerId); +} diff --git a/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs b/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs new file mode 100644 index 0000000000..57109ba6a7 --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/IVNextInMemoryApplicationCacheService.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public interface IVNextInMemoryApplicationCacheService +{ + Task> GetOrganizationAbilitiesAsync(); +#nullable enable + Task GetOrganizationAbilityAsync(Guid orgId); +#nullable disable + Task> GetProviderAbilitiesAsync(); + Task UpsertOrganizationAbilityAsync(Organization organization); + Task UpsertProviderAbilityAsync(Provider provider); + Task DeleteOrganizationAbilityAsync(Guid organizationId); + Task DeleteProviderAbilityAsync(Guid providerId); +} diff --git a/src/Core/AdminConsole/AbilitiesCache/NoOpApplicationCacheMessaging.cs b/src/Core/AdminConsole/AbilitiesCache/NoOpApplicationCacheMessaging.cs new file mode 100644 index 0000000000..36a380a850 --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/NoOpApplicationCacheMessaging.cs @@ -0,0 +1,21 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public class NoOpApplicationCacheMessaging : IApplicationCacheServiceBusMessaging +{ + public Task NotifyOrganizationAbilityUpsertedAsync(Organization organization) + { + return Task.CompletedTask; + } + + public Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId) + { + return Task.CompletedTask; + } + + public Task NotifyProviderAbilityDeletedAsync(Guid providerId) + { + return Task.CompletedTask; + } +} diff --git a/src/Core/AdminConsole/AbilitiesCache/ServiceBusApplicationCacheMessaging.cs b/src/Core/AdminConsole/AbilitiesCache/ServiceBusApplicationCacheMessaging.cs new file mode 100644 index 0000000000..f267871da7 --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/ServiceBusApplicationCacheMessaging.cs @@ -0,0 +1,63 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; +using Bit.Core.Settings; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public class ServiceBusApplicationCacheMessaging : IApplicationCacheServiceBusMessaging +{ + private readonly ServiceBusSender _topicMessageSender; + private readonly string _subName; + + public ServiceBusApplicationCacheMessaging( + GlobalSettings globalSettings) + { + _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings); + var serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString); + _topicMessageSender = serviceBusClient.CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName); + } + + public async Task NotifyOrganizationAbilityUpsertedAsync(Organization organization) + { + var message = new ServiceBusMessage + { + Subject = _subName, + ApplicationProperties = + { + { "type", (byte)ApplicationCacheMessageType.UpsertOrganizationAbility }, + { "id", organization.Id }, + } + }; + await _topicMessageSender.SendMessageAsync(message); + } + + public async Task NotifyOrganizationAbilityDeletedAsync(Guid organizationId) + { + var message = new ServiceBusMessage + { + Subject = _subName, + ApplicationProperties = + { + { "type", (byte)ApplicationCacheMessageType.DeleteOrganizationAbility }, + { "id", organizationId }, + } + }; + await _topicMessageSender.SendMessageAsync(message); + } + + public async Task NotifyProviderAbilityDeletedAsync(Guid providerId) + { + var message = new ServiceBusMessage + { + Subject = _subName, + ApplicationProperties = + { + { "type", (byte)ApplicationCacheMessageType.DeleteProviderAbility }, + { "id", providerId }, + } + }; + await _topicMessageSender.SendMessageAsync(message); + } +} diff --git a/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs b/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs new file mode 100644 index 0000000000..409074e3b2 --- /dev/null +++ b/src/Core/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheService.cs @@ -0,0 +1,137 @@ +using System.Collections.Concurrent; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; + +namespace Bit.Core.AdminConsole.AbilitiesCache; + +public class VNextInMemoryApplicationCacheService( + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + TimeProvider timeProvider) : IVNextInMemoryApplicationCacheService +{ + private ConcurrentDictionary _orgAbilities = new(); + private readonly SemaphoreSlim _orgInitLock = new(1, 1); + private DateTimeOffset _lastOrgAbilityRefresh = DateTimeOffset.MinValue; + + private ConcurrentDictionary _providerAbilities = new(); + private readonly SemaphoreSlim _providerInitLock = new(1, 1); + private DateTimeOffset _lastProviderAbilityRefresh = DateTimeOffset.MinValue; + + private readonly TimeSpan _refreshInterval = TimeSpan.FromMinutes(10); + + public virtual async Task> GetOrganizationAbilitiesAsync() + { + await InitOrganizationAbilitiesAsync(); + return _orgAbilities; + } + + public async Task GetOrganizationAbilityAsync(Guid organizationId) + { + (await GetOrganizationAbilitiesAsync()) + .TryGetValue(organizationId, out var organizationAbility); + return organizationAbility; + } + + public virtual async Task> GetProviderAbilitiesAsync() + { + await InitProviderAbilitiesAsync(); + return _providerAbilities; + } + + public virtual async Task UpsertProviderAbilityAsync(Provider provider) + { + await InitProviderAbilitiesAsync(); + _providerAbilities.AddOrUpdate( + provider.Id, + static (_, provider) => new ProviderAbility(provider), + static (_, _, provider) => new ProviderAbility(provider), + provider); + } + + public virtual async Task UpsertOrganizationAbilityAsync(Organization organization) + { + await InitOrganizationAbilitiesAsync(); + + _orgAbilities.AddOrUpdate( + organization.Id, + static (_, organization) => new OrganizationAbility(organization), + static (_, _, organization) => new OrganizationAbility(organization), + organization); + } + + public virtual Task DeleteOrganizationAbilityAsync(Guid organizationId) + { + _orgAbilities.TryRemove(organizationId, out _); + return Task.CompletedTask; + } + + public virtual Task DeleteProviderAbilityAsync(Guid providerId) + { + _providerAbilities.TryRemove(providerId, out _); + return Task.CompletedTask; + } + + private async Task InitOrganizationAbilitiesAsync() => + await InitAbilitiesAsync( + dict => _orgAbilities = dict, + () => _lastOrgAbilityRefresh, + dt => _lastOrgAbilityRefresh = dt, + _orgInitLock, + async () => await organizationRepository.GetManyAbilitiesAsync(), + _refreshInterval, + ability => ability.Id); + + private async Task InitProviderAbilitiesAsync() => + await InitAbilitiesAsync( + dict => _providerAbilities = dict, + () => _lastProviderAbilityRefresh, + dateTime => _lastProviderAbilityRefresh = dateTime, + _providerInitLock, + async () => await providerRepository.GetManyAbilitiesAsync(), + _refreshInterval, + ability => ability.Id); + + + private async Task InitAbilitiesAsync( + Action> setCache, + Func getLastRefresh, + Action setLastRefresh, + SemaphoreSlim @lock, + Func>> fetchFunc, + TimeSpan refreshInterval, + Func getId) + { + if (SkipRefresh()) + { + return; + } + + await @lock.WaitAsync(); + try + { + if (SkipRefresh()) + { + return; + } + + var sources = await fetchFunc(); + var abilities = new ConcurrentDictionary( + sources.ToDictionary(getId)); + setCache(abilities); + setLastRefresh(timeProvider.GetUtcNow()); + } + finally + { + @lock.Release(); + } + + bool SkipRefresh() + { + return timeProvider.GetUtcNow() - getLastRefresh() <= refreshInterval; + } + } +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ed9ee02dad..17dae1255c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -131,6 +131,8 @@ 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 DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; + public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; diff --git a/src/Core/HostedServices/ApplicationCacheHostedService.cs b/src/Core/HostedServices/ApplicationCacheHostedService.cs index ca2744bd10..655a713764 100644 --- a/src/Core/HostedServices/ApplicationCacheHostedService.cs +++ b/src/Core/HostedServices/ApplicationCacheHostedService.cs @@ -3,6 +3,7 @@ using Azure.Messaging.ServiceBus.Administration; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Services.Implementations; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.Extensions.Hosting; @@ -14,7 +15,7 @@ namespace Bit.Core.HostedServices; public class ApplicationCacheHostedService : IHostedService, IDisposable { - private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService; + private readonly FeatureRoutedCacheService? _applicationCacheService; private readonly IOrganizationRepository _organizationRepository; protected readonly ILogger _logger; private readonly ServiceBusClient _serviceBusClient; @@ -34,7 +35,7 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable { _topicName = globalSettings.ServiceBus.ApplicationCacheTopicName; _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings); - _applicationCacheService = applicationCacheService as InMemoryServiceBusApplicationCacheService; + _applicationCacheService = applicationCacheService as FeatureRoutedCacheService; _organizationRepository = organizationRepository; _logger = logger; _serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString); diff --git a/src/Core/Services/Implementations/FeatureRoutedCacheService.cs b/src/Core/Services/Implementations/FeatureRoutedCacheService.cs new file mode 100644 index 0000000000..b6294a28f8 --- /dev/null +++ b/src/Core/Services/Implementations/FeatureRoutedCacheService.cs @@ -0,0 +1,152 @@ +using Bit.Core.AdminConsole.AbilitiesCache; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Core.Services.Implementations; + +/// +/// A feature-flagged routing service for application caching that bridges the gap between +/// scoped dependency injection (IFeatureService) and singleton services (cache implementations). +/// This service allows dynamic routing between IVCurrentInMemoryApplicationCacheService and +/// IVNextInMemoryApplicationCacheService based on the PM23845_VNextApplicationCache feature flag. +/// +/// +/// This service is necessary because: +/// - IFeatureService is registered as Scoped in the DI container +/// - IVNextInMemoryApplicationCacheService and IVCurrentInMemoryApplicationCacheService are registered as Singleton +/// - We need to evaluate feature flags at request time while maintaining singleton cache behavior +/// +/// The service acts as a scoped proxy that can access the scoped IFeatureService while +/// delegating actual cache operations to the appropriate singleton implementation. +/// +public class FeatureRoutedCacheService( + IFeatureService featureService, + IVNextInMemoryApplicationCacheService vNextInMemoryApplicationCacheService, + IVCurrentInMemoryApplicationCacheService inMemoryApplicationCacheService, + IApplicationCacheServiceBusMessaging serviceBusMessaging) + : IApplicationCacheService +{ + public async Task> GetOrganizationAbilitiesAsync() + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + return await vNextInMemoryApplicationCacheService.GetOrganizationAbilitiesAsync(); + } + + return await inMemoryApplicationCacheService.GetOrganizationAbilitiesAsync(); + } + + public async Task GetOrganizationAbilityAsync(Guid orgId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + return await vNextInMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId); + } + return await inMemoryApplicationCacheService.GetOrganizationAbilityAsync(orgId); + } + + public async Task> GetProviderAbilitiesAsync() + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + return await vNextInMemoryApplicationCacheService.GetProviderAbilitiesAsync(); + } + return await inMemoryApplicationCacheService.GetProviderAbilitiesAsync(); + } + + public async Task UpsertOrganizationAbilityAsync(Organization organization) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization); + await serviceBusMessaging.NotifyOrganizationAbilityUpsertedAsync(organization); + } + else + { + await inMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization); + } + } + + public async Task UpsertProviderAbilityAsync(Provider provider) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider); + } + else + { + await inMemoryApplicationCacheService.UpsertProviderAbilityAsync(provider); + } + } + + public async Task DeleteOrganizationAbilityAsync(Guid organizationId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId); + await serviceBusMessaging.NotifyOrganizationAbilityDeletedAsync(organizationId); + } + else + { + await inMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId); + } + } + + public async Task DeleteProviderAbilityAsync(Guid providerId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId); + await serviceBusMessaging.NotifyProviderAbilityDeletedAsync(providerId); + } + else + { + await inMemoryApplicationCacheService.DeleteProviderAbilityAsync(providerId); + } + + } + + public async Task BaseUpsertOrganizationAbilityAsync(Organization organization) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.UpsertOrganizationAbilityAsync(organization); + } + else + { + // NOTE: This is a temporary workaround InMemoryServiceBusApplicationCacheService legacy implementation. + // Avoid using this approach in new code. + if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache) + { + await serviceBusCache.BaseUpsertOrganizationAbilityAsync(organization); + } + else + { + throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}"); + } + } + } + + public async Task BaseDeleteOrganizationAbilityAsync(Guid organizationId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache)) + { + await vNextInMemoryApplicationCacheService.DeleteOrganizationAbilityAsync(organizationId); + } + else + { + // NOTE: This is a temporary workaround InMemoryServiceBusApplicationCacheService legacy implementation. + // Avoid using this approach in new code. + if (inMemoryApplicationCacheService is InMemoryServiceBusApplicationCacheService serviceBusCache) + { + await serviceBusCache.BaseDeleteOrganizationAbilityAsync(organizationId); + } + else + { + throw new InvalidOperationException($"Expected {nameof(inMemoryApplicationCacheService)} to be of type {nameof(InMemoryServiceBusApplicationCacheService)}"); + } + } + } +} diff --git a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs index d1bece56c1..4062162701 100644 --- a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.AdminConsole.AbilitiesCache; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; @@ -10,7 +11,7 @@ using Bit.Core.Repositories; namespace Bit.Core.Services; -public class InMemoryApplicationCacheService : IApplicationCacheService +public class InMemoryApplicationCacheService : IVCurrentInMemoryApplicationCacheService { private readonly IOrganizationRepository _organizationRepository; private readonly IProviderRepository _providerRepository; diff --git a/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs index da70ccd2fd..b856bfa749 100644 --- a/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryServiceBusApplicationCacheService.cs @@ -8,9 +8,8 @@ using Bit.Core.Utilities; namespace Bit.Core.Services; -public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCacheService, IApplicationCacheService +public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCacheService { - private readonly ServiceBusClient _serviceBusClient; private readonly ServiceBusSender _topicMessageSender; private readonly string _subName; @@ -21,7 +20,7 @@ public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCach : base(organizationRepository, providerRepository) { _subName = CoreHelpers.GetApplicationCacheServiceBusSubscriptionName(globalSettings); - _serviceBusClient = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString); + _topicMessageSender = new ServiceBusClient(globalSettings.ServiceBus.ConnectionString).CreateSender(globalSettings.ServiceBus.ApplicationCacheTopicName); } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index fdeaad04b2..cfe177aa2c 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,7 +1,9 @@ using System.Globalization; +using Bit.Core.AdminConsole.AbilitiesCache; using Bit.Core.Auth.IdentityServer; using Bit.Core.Context; using Bit.Core.Services; +using Bit.Core.Services.Implementations; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; @@ -52,13 +54,18 @@ public class Startup // Services var usingServiceBusAppCache = CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName); + services.AddScoped(); + services.AddSingleton(); + if (usingServiceBusAppCache) { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } else { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } services.AddEventWriteServices(globalSettings); diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 592f7c84c3..d87f9ab97f 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; +using Bit.Core.AdminConsole.AbilitiesCache; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; @@ -41,6 +42,7 @@ using Bit.Core.Resources; using Bit.Core.SecretsManager.Repositories; using Bit.Core.SecretsManager.Repositories.Noop; using Bit.Core.Services; +using Bit.Core.Services.Implementations; using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Tools.ImportFeatures; @@ -247,14 +249,19 @@ public static class ServiceCollectionExtensions services.AddOptionality(); services.AddTokenizers(); + services.AddSingleton(); + services.AddScoped(); + if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } else { - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret); diff --git a/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs b/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs new file mode 100644 index 0000000000..afd3dccda3 --- /dev/null +++ b/test/Core.Test/AdminConsole/AbilitiesCache/VNextInMemoryApplicationCacheServiceTests.cs @@ -0,0 +1,403 @@ +using System.Collections.Concurrent; +using Bit.Core.AdminConsole.AbilitiesCache; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.AbilitiesCache; + +[SutProviderCustomize] +public class VNextInMemoryApplicationCacheServiceTests + +{ + [Theory, BitAutoData] + public async Task GetOrganizationAbilitiesAsync_FirstCall_LoadsFromRepository( + ICollection organizationAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.IsType>(result); + Assert.Equal(organizationAbilities.Count, result.Count); + foreach (var ability in organizationAbilities) + { + Assert.True(result.TryGetValue(ability.Id, out var actualAbility)); + Assert.Equal(ability, actualAbility); + } + await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilitiesAsync_SecondCall_UsesCachedValue( + List organizationAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + // Act + var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.Same(firstCall, secondCall); + await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilityAsync_ExistingId_ReturnsAbility( + List organizationAbilities, + SutProvider sutProvider) + { + // Arrange + var targetAbility = organizationAbilities.First(); + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilityAsync(targetAbility.Id); + + // Assert + Assert.Equal(targetAbility, result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilityAsync_NonExistingId_ReturnsNull( + List organizationAbilities, + Guid nonExistingId, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilityAsync(nonExistingId); + + // Assert + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetProviderAbilitiesAsync_FirstCall_LoadsFromRepository( + List providerAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(providerAbilities); + + // Act + var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.IsType>(result); + Assert.Equal(providerAbilities.Count, result.Count); + foreach (var ability in providerAbilities) + { + Assert.True(result.TryGetValue(ability.Id, out var actualAbility)); + Assert.Equal(ability, actualAbility); + } + await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetProviderAbilitiesAsync_SecondCall_UsesCachedValue( + List providerAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(providerAbilities); + + // Act + var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.Same(firstCall, secondCall); + await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task UpsertOrganizationAbilityAsync_NewOrganization_AddsToCache( + Organization organization, + List existingAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(existingAbilities); + await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Act + await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); + + // Assert + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + Assert.True(result.ContainsKey(organization.Id)); + Assert.Equal(organization.Id, result[organization.Id].Id); + } + + [Theory, BitAutoData] + public async Task UpsertOrganizationAbilityAsync_ExistingOrganization_UpdatesCache( + Organization organization, + List existingAbilities, + SutProvider sutProvider) + { + // Arrange + existingAbilities.Add(new OrganizationAbility { Id = organization.Id }); + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(existingAbilities); + await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Act + await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); + + // Assert + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + Assert.True(result.ContainsKey(organization.Id)); + Assert.Equal(organization.Id, result[organization.Id].Id); + } + + [Theory, BitAutoData] + public async Task UpsertProviderAbilityAsync_NewProvider_AddsToCache( + Provider provider, + List existingAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(existingAbilities); + await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Act + await sutProvider.Sut.UpsertProviderAbilityAsync(provider); + + // Assert + var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); + Assert.True(result.ContainsKey(provider.Id)); + Assert.Equal(provider.Id, result[provider.Id].Id); + } + + [Theory, BitAutoData] + public async Task DeleteOrganizationAbilityAsync_ExistingId_RemovesFromCache( + List organizationAbilities, + SutProvider sutProvider) + { + // Arrange + var targetAbility = organizationAbilities.First(); + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Act + await sutProvider.Sut.DeleteOrganizationAbilityAsync(targetAbility.Id); + + // Assert + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + Assert.False(result.ContainsKey(targetAbility.Id)); + } + + + [Theory, BitAutoData] + public async Task DeleteProviderAbilityAsync_ExistingId_RemovesFromCache( + List providerAbilities, + SutProvider sutProvider) + { + // Arrange + var targetAbility = providerAbilities.First(); + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(providerAbilities); + await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Act + await sutProvider.Sut.DeleteProviderAbilityAsync(targetAbility.Id); + + // Assert + var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); + Assert.False(result.ContainsKey(targetAbility.Id)); + } + + [Theory, BitAutoData] + public async Task ConcurrentAccess_GetOrganizationAbilities_ThreadSafe( + List organizationAbilities, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + var results = new ConcurrentBag>(); + + const int iterationCount = 100; + + + // Act + await Parallel.ForEachAsync( + Enumerable.Range(0, iterationCount), + async (_, _) => + { + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + results.Add(result); + }); + + // Assert + var firstCall = results.First(); + Assert.Equal(iterationCount, results.Count); + Assert.All(results, result => Assert.Same(firstCall, result)); + await sutProvider.GetDependency().Received(1).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository( + List organizationAbilities, + List updatedAbilities) + { + // Arrange + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities, updatedAbilities); + + var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + const int pastIntervalInMinutes = 11; + SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes); + + // Act + var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.NotSame(firstCall, secondCall); + Assert.Equal(updatedAbilities.Count, secondCall.Count); + await sutProvider.GetDependency().Received(2).GetManyAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetProviderAbilitiesAsync_AfterRefreshInterval_RefreshesFromRepository( + List providerAbilities, + List updatedAbilities) + { + // Arrange + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(providerAbilities, updatedAbilities); + + var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + const int pastIntervalMinutes = 15; + SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalMinutes); + + // Act + var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.NotSame(firstCall, secondCall); + Assert.Equal(updatedAbilities.Count, secondCall.Count); + await sutProvider.GetDependency().Received(2).GetManyAbilitiesAsync(); + } + + public static IEnumerable WhenCacheIsWithinIntervalTestCases => + [ + [5, 1], + [10, 1], + ]; + + [Theory] + [BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))] + public async Task GetOrganizationAbilitiesAsync_WhenCacheIsWithinInterval( + int pastIntervalInMinutes, + int expectCacheHit, + List organizationAbilities) + { + // Arrange + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(organizationAbilities); + + var firstCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes); + + // Act + var secondCall = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.Same(firstCall, secondCall); + Assert.Equal(organizationAbilities.Count, secondCall.Count); + await sutProvider.GetDependency().Received(expectCacheHit).GetManyAbilitiesAsync(); + } + + [Theory] + [BitMemberAutoData(nameof(WhenCacheIsWithinIntervalTestCases))] + public async Task GetProviderAbilitiesAsync_WhenCacheIsWithinInterval( + int pastIntervalInMinutes, + int expectCacheHit, + List providerAbilities) + { + // Arrange + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + + sutProvider.GetDependency() + .GetManyAbilitiesAsync() + .Returns(providerAbilities); + + var firstCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + SimulateTimeLapseAfterFirstCall(sutProvider, pastIntervalInMinutes); + + // Act + var secondCall = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.Same(firstCall, secondCall); + Assert.Equal(providerAbilities.Count, secondCall.Count); + await sutProvider.GetDependency().Received(expectCacheHit).GetManyAbilitiesAsync(); + } + + private static void SimulateTimeLapseAfterFirstCall(SutProvider sutProvider, int pastIntervalInMinutes) => + sutProvider + .GetDependency() + .Advance(TimeSpan.FromMinutes(pastIntervalInMinutes)); + +} diff --git a/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs b/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs new file mode 100644 index 0000000000..3309f2bf23 --- /dev/null +++ b/test/Core.Test/Services/Implementations/FeatureRoutedCacheServiceTests.cs @@ -0,0 +1,541 @@ +using Bit.Core.AdminConsole.AbilitiesCache; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Services.Implementations; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +using GlobalSettings = Bit.Core.Settings.GlobalSettings; + +namespace Bit.Core.Test.Services.Implementations; + +[SutProviderCustomize] +public class FeatureRoutedCacheServiceTests +{ + [Theory, BitAutoData] + public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService( + SutProvider sutProvider, + IDictionary expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationAbilitiesAsync(); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( + SutProvider sutProvider, + IDictionary expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilitiesAsync(); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationAbilitiesAsync(); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilityAsync_WhenFeatureIsEnabled_ReturnsFromVNextService( + SutProvider sutProvider, + Guid orgId, + OrganizationAbility expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(orgId) + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationAbilityAsync(orgId); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationAbilityAsync(orgId); + } + + [Theory, BitAutoData] + public async Task GetOrganizationAbilityAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( + SutProvider sutProvider, + Guid orgId, + OrganizationAbility expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + sutProvider.GetDependency() + .GetOrganizationAbilityAsync(orgId) + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetOrganizationAbilityAsync(orgId); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetOrganizationAbilityAsync(orgId); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetOrganizationAbilityAsync(orgId); + } + + [Theory, BitAutoData] + public async Task GetProviderAbilitiesAsync_WhenFeatureIsEnabled_ReturnsFromVNextService( + SutProvider sutProvider, + IDictionary expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + sutProvider.GetDependency() + .GetProviderAbilitiesAsync() + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetProviderAbilitiesAsync(); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetProviderAbilitiesAsync(); + } + + + [Theory, BitAutoData] + public async Task GetProviderAbilitiesAsync_WhenFeatureIsDisabled_ReturnsFromInMemoryService( + SutProvider sutProvider, + IDictionary expectedResult) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + sutProvider.GetDependency() + .GetProviderAbilitiesAsync() + .Returns(expectedResult); + + // Act + var result = await sutProvider.Sut.GetProviderAbilitiesAsync(); + + // Assert + Assert.Equal(expectedResult, result); + await sutProvider.GetDependency() + .Received(1) + .GetProviderAbilitiesAsync(); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetProviderAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Organization organization) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetProviderAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task UpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( + SutProvider sutProvider, + Organization organization) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + await sutProvider.Sut.UpsertOrganizationAbilityAsync(organization); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + + await sutProvider.GetDependency() + .DidNotReceive() + .GetProviderAbilitiesAsync(); + } + + [Theory, BitAutoData] + public async Task UpsertProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Provider provider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.UpsertProviderAbilityAsync(provider); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertProviderAbilityAsync(provider); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertProviderAbilityAsync(provider); + } + + [Theory, BitAutoData] + public async Task UpsertProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( + SutProvider sutProvider, + Provider provider) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + await sutProvider.Sut.UpsertProviderAbilityAsync(provider); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertProviderAbilityAsync(provider); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertProviderAbilityAsync(provider); + } + + [Theory, BitAutoData] + public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(organizationId); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteOrganizationAbilityAsync(organizationId); + } + + [Theory, BitAutoData] + public async Task DeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + await sutProvider.Sut.DeleteOrganizationAbilityAsync(organizationId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(organizationId); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteOrganizationAbilityAsync(organizationId); + } + + [Theory, BitAutoData] + public async Task DeleteProviderAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteProviderAbilityAsync(providerId); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteProviderAbilityAsync(providerId); + } + + [Theory, BitAutoData] + public async Task DeleteProviderAbilityAsync_WhenFeatureIsDisabled_CallsInMemoryService( + SutProvider sutProvider, + Guid providerId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + await sutProvider.Sut.DeleteProviderAbilityAsync(providerId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteProviderAbilityAsync(providerId); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteProviderAbilityAsync(providerId); + } + + [Theory, BitAutoData] + public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Organization organization) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache( + Organization organization) + { + // Arrange + var featureService = Substitute.For(); + + var currentCacheService = CreateCurrentCacheMockService(); + + featureService + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + var sutProvider = Substitute.For( + featureService, + Substitute.For(), + currentCacheService, + Substitute.For()); + + // Act + await sutProvider.BaseUpsertOrganizationAbilityAsync(organization); + + // Assert + await currentCacheService + .Received(1) + .BaseUpsertOrganizationAbilityAsync(organization); + } + + /// + /// Our SUT is using a method that is not part of the IVCurrentInMemoryApplicationCacheService, + /// so AutoFixture’s auto-created mock won’t work. + /// + /// + private static InMemoryServiceBusApplicationCacheService CreateCurrentCacheMockService() + { + var currentCacheService = Substitute.For( + Substitute.For(), + Substitute.For(), + new GlobalSettings + { + ProjectName = "BitwardenTest", + ServiceBus = new GlobalSettings.ServiceBusSettings + { + ConnectionString = "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=test", + ApplicationCacheTopicName = "test-topic", + ApplicationCacheSubscriptionName = "test-subscription" + } + }); + return currentCacheService; + } + + [Theory, BitAutoData] + public async Task BaseUpsertOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException( + SutProvider sutProvider, + Organization organization) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + var ex = await Assert.ThrowsAsync( + () => sutProvider.Sut.BaseUpsertOrganizationAbilityAsync(organization)); + + // Assert + Assert.Equal( + ExpectedErrorMessage, + ex.Message); + } + + private static string ExpectedErrorMessage + { + get => "Expected inMemoryApplicationCacheService to be of type InMemoryServiceBusApplicationCacheService"; + } + + [Theory, BitAutoData] + public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsEnabled_CallsVNextService( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(true); + + // Act + await sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(organizationId); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteOrganizationAbilityAsync(organizationId); + } + + [Theory, BitAutoData] + public async Task BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_CallsServiceBusCache( + Guid organizationId) + { + // Arrange + var featureService = Substitute.For(); + + var currentCacheService = CreateCurrentCacheMockService(); + + featureService + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + var sutProvider = Substitute.For( + featureService, + Substitute.For(), + currentCacheService, + Substitute.For()); + + // Act + await sutProvider.BaseDeleteOrganizationAbilityAsync(organizationId); + + // Assert + await currentCacheService + .Received(1) + .BaseDeleteOrganizationAbilityAsync(organizationId); + } + + [Theory, BitAutoData] + public async Task + BaseDeleteOrganizationAbilityAsync_WhenFeatureIsDisabled_AndServiceIsNotServiceBusCache_ThrowsException( + SutProvider sutProvider, + Guid organizationId) + { + // Arrange + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM23845_VNextApplicationCache) + .Returns(false); + + // Act + var ex = await Assert.ThrowsAsync(() => + sutProvider.Sut.BaseDeleteOrganizationAbilityAsync(organizationId)); + + // Assert + Assert.Equal( + ExpectedErrorMessage, + ex.Message); + } +} From 6ade09312fcca883d199dc2e7e3049c0f76f3f54 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 12 Sep 2025 11:49:40 -0700 Subject: [PATCH 236/326] [PM-21044] - optimize security task ReadByUserIdStatus (#5779) * optimize security task ReadByUserIdStatus * fix AccessibleCiphers query * fix error * add migrator file * fix migration * update sproc * mirror sprocs * revert change to sproc * add indexes. update filename. add GO statement * move index declarations to appropriate files * add missing GO statement * select view. add existance checks for index * update indexes * revert changes * rename file * update security task * update sproc * update script file * bump migration date * add filtered index. update statistics, update description with perf metics * rename file * reordering * remove update statistics * remove update statistics * add missing index * fix sproc * update timestamp * improve sproc with de-dupe and views * fix syntax error * add missing inner join * sync up index * fix indentation * update file timestamp * remove unnecessary indexes. update sql to match guidelines. * add comment for status * add comment for status --- src/Sql/dbo/Tables/CollectionCipher.sql | 1 + src/Sql/dbo/Tables/OrganizationUser.sql | 7 + .../SecurityTask_ReadByUserIdStatus.sql | 123 +++++++++++------- src/Sql/dbo/Vault/Tables/SecurityTask.sql | 3 + .../2025-09-03_00_ImproveSecurityTask.sql | 101 ++++++++++++++ 5 files changed, 189 insertions(+), 46 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-09-03_00_ImproveSecurityTask.sql diff --git a/src/Sql/dbo/Tables/CollectionCipher.sql b/src/Sql/dbo/Tables/CollectionCipher.sql index f661cb6fbd..0891b7bc42 100644 --- a/src/Sql/dbo/Tables/CollectionCipher.sql +++ b/src/Sql/dbo/Tables/CollectionCipher.sql @@ -11,3 +11,4 @@ GO CREATE NONCLUSTERED INDEX [IX_CollectionCipher_CipherId] ON [dbo].[CollectionCipher]([CipherId] ASC); +GO diff --git a/src/Sql/dbo/Tables/OrganizationUser.sql b/src/Sql/dbo/Tables/OrganizationUser.sql index 51ed2115bc..a9f228dc3d 100644 --- a/src/Sql/dbo/Tables/OrganizationUser.sql +++ b/src/Sql/dbo/Tables/OrganizationUser.sql @@ -35,3 +35,10 @@ CREATE NONCLUSTERED INDEX [IX_OrganizationUser_OrganizationId_UserId] INCLUDE ([Email], [Status], [Type], [ExternalId], [CreationDate], [RevisionDate], [Permissions], [ResetPasswordKey], [AccessSecretsManager]); GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserId_Status_Filtered] + ON [dbo].[OrganizationUser] ([UserId]) + INCLUDE ([Id], [OrganizationId]) + WHERE [Status] = 2; -- Confirmed + +GO diff --git a/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql index 2a4ecdb4c1..2614135c54 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql @@ -1,56 +1,87 @@ CREATE PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus] - @UserId UNIQUEIDENTIFIER, - @Status TINYINT = NULL + @UserId [UNIQUEIDENTIFIER], + @Status [TINYINT] = NULL AS BEGIN - SET NOCOUNT ON + SET NOCOUNT ON; + WITH [OrganizationAccess] AS ( + SELECT + [OU].[OrganizationId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [O].[Enabled] = 1 + ), + [UserCollectionAccess] AS ( + SELECT + [CC].[CipherId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + INNER JOIN [dbo].[CollectionUser] [CU] + ON [CU].[OrganizationUserId] = [OU].[Id] + INNER JOIN [dbo].[CollectionCipher] [CC] + ON [CC].[CollectionId] = [CU].[CollectionId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [O].[Enabled] = 1 + AND [CU].[ReadOnly] = 0 + ), + [GroupCollectionAccess] AS ( + SELECT + [CC].[CipherId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + INNER JOIN [dbo].[GroupUser] [GU] + ON [GU].[OrganizationUserId] = [OU].[Id] + INNER JOIN [dbo].[CollectionGroup] [CG] + ON [CG].[GroupId] = [GU].[GroupId] + INNER JOIN [dbo].[CollectionCipher] [CC] + ON [CC].[CollectionId] = [CG].[CollectionId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [CG].[ReadOnly] = 0 + ), + [AccessibleCiphers] AS ( + SELECT + [CipherId] FROM [UserCollectionAccess] + UNION + SELECT + [CipherId] FROM [GroupCollectionAccess] + ) SELECT - ST.Id, - ST.OrganizationId, - ST.CipherId, - ST.Type, - ST.Status, - ST.CreationDate, - ST.RevisionDate + [ST].[Id], + [ST].[OrganizationId], + [ST].[CipherId], + [ST].[Type], + [ST].[Status], + [ST].[CreationDate], + [ST].[RevisionDate] FROM - [dbo].[SecurityTaskView] ST - INNER JOIN - [dbo].[OrganizationUserView] OU ON OU.[OrganizationId] = ST.[OrganizationId] - INNER JOIN - [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId] - LEFT JOIN - [dbo].[CipherView] C ON C.[Id] = ST.[CipherId] - LEFT JOIN - [dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id] AND C.[Id] IS NOT NULL - LEFT JOIN - [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id] AND C.[Id] IS NOT NULL - LEFT JOIN - [dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] IS NULL AND C.[Id] IS NOT NULL - LEFT JOIN - [dbo].[CollectionGroup] CG ON CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = CC.[CollectionId] + [dbo].[SecurityTaskView] [ST] + INNER JOIN [OrganizationAccess] [OA] + ON [ST].[OrganizationId] = [OA].[OrganizationId] WHERE - OU.[UserId] = @UserId - AND OU.[Status] = 2 -- Ensure user is confirmed - AND O.[Enabled] = 1 + (@Status IS NULL OR [ST].[Status] = @Status) AND ( - ST.[CipherId] IS NULL - OR ( - C.[Id] IS NOT NULL - AND ( - CU.[ReadOnly] = 0 - OR CG.[ReadOnly] = 0 - ) - ) + [ST].[CipherId] IS NULL + OR EXISTS ( + SELECT 1 + FROM [AccessibleCiphers] [AC] + WHERE [AC].[CipherId] = [ST].[CipherId] + ) ) - AND ST.[Status] = COALESCE(@Status, ST.[Status]) - GROUP BY - ST.Id, - ST.OrganizationId, - ST.CipherId, - ST.Type, - ST.Status, - ST.CreationDate, - ST.RevisionDate - ORDER BY ST.[CreationDate] DESC + ORDER BY + [ST].[CreationDate] DESC + OPTION (RECOMPILE); END diff --git a/src/Sql/dbo/Vault/Tables/SecurityTask.sql b/src/Sql/dbo/Vault/Tables/SecurityTask.sql index a00dcede9c..dbf9827a63 100644 --- a/src/Sql/dbo/Vault/Tables/SecurityTask.sql +++ b/src/Sql/dbo/Vault/Tables/SecurityTask.sql @@ -19,3 +19,6 @@ CREATE NONCLUSTERED INDEX [IX_SecurityTask_CipherId] GO CREATE NONCLUSTERED INDEX [IX_SecurityTask_OrganizationId] ON [dbo].[SecurityTask]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL; + +GO + diff --git a/util/Migrator/DbScripts/2025-09-03_00_ImproveSecurityTask.sql b/util/Migrator/DbScripts/2025-09-03_00_ImproveSecurityTask.sql new file mode 100644 index 0000000000..743caf4672 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-03_00_ImproveSecurityTask.sql @@ -0,0 +1,101 @@ +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadByUserIdStatus] + @UserId [UNIQUEIDENTIFIER], + @Status [TINYINT] = NULL +AS +BEGIN + SET NOCOUNT ON; + + WITH [OrganizationAccess] AS ( + SELECT + [OU].[OrganizationId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [O].[Enabled] = 1 + ), + [UserCollectionAccess] AS ( + SELECT + [CC].[CipherId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + INNER JOIN [dbo].[CollectionUser] [CU] + ON [CU].[OrganizationUserId] = [OU].[Id] + INNER JOIN [dbo].[CollectionCipher] [CC] + ON [CC].[CollectionId] = [CU].[CollectionId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [O].[Enabled] = 1 + AND [CU].[ReadOnly] = 0 + ), + [GroupCollectionAccess] AS ( + SELECT + [CC].[CipherId] + FROM + [dbo].[OrganizationUser] [OU] + INNER JOIN [dbo].[OrganizationView] [O] + ON [O].[Id] = [OU].[OrganizationId] + INNER JOIN [dbo].[GroupUser] [GU] + ON [GU].[OrganizationUserId] = [OU].[Id] + INNER JOIN [dbo].[CollectionGroup] [CG] + ON [CG].[GroupId] = [GU].[GroupId] + INNER JOIN [dbo].[CollectionCipher] [CC] + ON [CC].[CollectionId] = [CG].[CollectionId] + WHERE + [OU].[UserId] = @UserId + AND [OU].[Status] = 2 -- Confirmed + AND [CG].[ReadOnly] = 0 + ), + [AccessibleCiphers] AS ( + SELECT + [CipherId] FROM [UserCollectionAccess] + UNION + SELECT + [CipherId] FROM [GroupCollectionAccess] + ) + SELECT + [ST].[Id], + [ST].[OrganizationId], + [ST].[CipherId], + [ST].[Type], + [ST].[Status], + [ST].[CreationDate], + [ST].[RevisionDate] + FROM + [dbo].[SecurityTaskView] [ST] + INNER JOIN [OrganizationAccess] [OA] + ON [ST].[OrganizationId] = [OA].[OrganizationId] + WHERE + (@Status IS NULL OR [ST].[Status] = @Status) + AND ( + [ST].[CipherId] IS NULL + OR EXISTS ( + SELECT 1 + FROM [AccessibleCiphers] [AC] + WHERE [AC].[CipherId] = [ST].[CipherId] + ) + ) + ORDER BY + [ST].[CreationDate] DESC + OPTION (RECOMPILE); +END +GO + +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE object_id = OBJECT_ID('dbo.OrganizationUser') + AND name = 'IX_OrganizationUser_UserId_Status_Filtered' +) +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserId_Status_Filtered] + ON [dbo].[OrganizationUser] ([UserId]) + INCLUDE ([Id], [OrganizationId]) + WHERE [Status] = 2; -- Confirmed +END From b4a0555a72a6b538ec04e0598a9543ce33b2b6a0 Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Mon, 15 Sep 2025 15:02:40 +0200 Subject: [PATCH 237/326] Change swagger docs to refer to main (#6337) --- src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs index cbad1e9736..fba8b17078 100644 --- a/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs +++ b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs @@ -17,8 +17,6 @@ namespace Bit.SharedWeb.Swagger; /// public class SourceFileLineOperationFilter : IOperationFilter { - private static readonly string _gitCommit = GitCommitDocumentFilter.GitCommit ?? "main"; - public void Apply(OpenApiOperation operation, OperationFilterContext context) { @@ -27,7 +25,7 @@ public class SourceFileLineOperationFilter : IOperationFilter { // Add the information with a link to the source file at the end of the operation description operation.Description += - $"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/{_gitCommit}/{fileName}#L{lineNumber}`]"; + $"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/main/{fileName}#L{lineNumber}`]"; // Also add the information as extensions, so other tools can use it in the future operation.Extensions.Add("x-source-file", new OpenApiString(fileName)); From b249c4e4d7ddaf0646794ca8f5b40fe1fb9566ad Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 15 Sep 2025 08:22:39 -0500 Subject: [PATCH 238/326] [PM-23761] Auto-reply to tickets in Freskdesk with help from Onyx AI (#6315) --- src/Billing/Billing.csproj | 1 + src/Billing/BillingSettings.cs | 4 + .../Controllers/FreshdeskController.cs | 94 +++++++++++++++++++ .../Models/FreshdeskReplyRequestModel.cs | 9 ++ src/Billing/appsettings.json | 5 +- 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/Billing/Models/FreshdeskReplyRequestModel.cs diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 18c627c5de..d6eb2b4411 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Billing/BillingSettings.cs b/src/Billing/BillingSettings.cs index 5609879eeb..32630e4a4a 100644 --- a/src/Billing/BillingSettings.cs +++ b/src/Billing/BillingSettings.cs @@ -34,6 +34,10 @@ public class BillingSettings public virtual string Region { get; set; } public virtual string UserFieldName { get; set; } public virtual string OrgFieldName { get; set; } + + public virtual bool RemoveNewlinesInReplies { get; set; } = false; + public virtual string AutoReplyGreeting { get; set; } = string.Empty; + public virtual string AutoReplySalutation { get; set; } = string.Empty; } public class OnyxSettings diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index a854d2d49f..66d4f47d92 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -11,6 +11,7 @@ using Bit.Billing.Models; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Utilities; +using Markdig; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -184,6 +185,52 @@ public class FreshdeskController : Controller return Ok(); } + [HttpPost("webhook-onyx-ai-reply")] + public async Task PostWebhookOnyxAiReply([FromQuery, Required] string key, + [FromBody, Required] FreshdeskOnyxAiWebhookModel model) + { + // NOTE: + // at this time, this endpoint is a duplicate of `webhook-onyx-ai` + // eventually, we will merge both endpoints into one webhook for Freshdesk + + // ensure that the key is from Freshdesk + if (!IsValidRequestFromFreshdesk(key) || !ModelState.IsValid) + { + return new BadRequestResult(); + } + + // if there is no description, then we don't send anything to onyx + if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim())) + { + 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); + + // the CallOnyxApi will return a null if we have an error response + if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.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)); + + return Ok(); // return ok so we don't retry + } + + // add the reply to the ticket + await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId); + + return Ok(); + } + private bool IsValidRequestFromFreshdesk(string key) { if (string.IsNullOrWhiteSpace(key) @@ -238,6 +285,53 @@ public class FreshdeskController : Controller } } + private async Task AddReplyToTicketAsync(string note, string ticketId) + { + // if there is no content, then we don't need to add a note + if (string.IsNullOrWhiteSpace(note)) + { + return; + } + + // convert note from markdown to html + var htmlNote = note; + try + { + var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build(); + htmlNote = Markdig.Markdown.ToHtml(note, pipeline); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error converting markdown to HTML for Freshdesk reply. Ticket Id: {0}. Note: {1}", + ticketId, note); + htmlNote = note; // fallback to the original note + } + + // clear out any new lines that Freshdesk doesn't like + if (_billingSettings.FreshDesk.RemoveNewlinesInReplies) + { + htmlNote = htmlNote.Replace(Environment.NewLine, string.Empty); + } + + var replyBody = new FreshdeskReplyRequestModel + { + Body = $"{_billingSettings.FreshDesk.AutoReplyGreeting}{htmlNote}{_billingSettings.FreshDesk.AutoReplySalutation}", + }; + + var replyRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/reply", ticketId)) + { + Content = JsonContent.Create(replyBody), + }; + + var addReplyResponse = await CallFreshdeskApiAsync(replyRequest); + if (addReplyResponse.StatusCode != System.Net.HttpStatusCode.Created) + { + _logger.LogError("Error adding reply to Freshdesk ticket. Ticket Id: {0}. Status: {1}", + ticketId, addReplyResponse.ToString()); + } + } + private async Task CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0) { try diff --git a/src/Billing/Models/FreshdeskReplyRequestModel.cs b/src/Billing/Models/FreshdeskReplyRequestModel.cs new file mode 100644 index 0000000000..3927039769 --- /dev/null +++ b/src/Billing/Models/FreshdeskReplyRequestModel.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Bit.Billing.Models; + +public class FreshdeskReplyRequestModel +{ + [JsonPropertyName("body")] + public required string Body { get; set; } +} diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index aae25dde0b..0074b5aafe 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -72,7 +72,10 @@ "webhookKey": "SECRET", "region": "US", "userFieldName": "cf_user", - "orgFieldName": "cf_org" + "orgFieldName": "cf_org", + "removeNewlinesInReplies": true, + "autoReplyGreeting": "Greetings,

Thank you for contacting Bitwarden. The reply below was generated by our AI agent based on your message:

", + "autoReplySalutation": "

If this response doesn’t fully address your question, simply reply to this email and a member of our Customer Success team will be happy to assist you further.

Best Regards,
The Bitwarden Customer Success Team

" }, "onyx": { "apiKey": "SECRET", From 981ff51d5785e1ea8ab7751010e02fa6fdd7a968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 15 Sep 2025 16:49:46 +0200 Subject: [PATCH 239/326] Update swashbuckle to fix API docs (#6319) --- .config/dotnet-tools.json | 2 +- src/Api/Api.csproj | 2 +- src/Billing/Billing.csproj | 2 +- src/SharedWeb/SharedWeb.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 41674ccad0..227f59ad8a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "9.0.2", + "version": "9.0.4", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index d48f49626f..138549e92d 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -34,7 +34,7 @@ - + diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index d6eb2b4411..e2b7447eb7 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj index 445b98cce0..8bffa285fc 100644 --- a/src/SharedWeb/SharedWeb.csproj +++ b/src/SharedWeb/SharedWeb.csproj @@ -7,7 +7,7 @@ - + From 0ee307a027ad534c9ced56239b8ca66b1276aa7f Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:56:33 -0400 Subject: [PATCH 240/326] [PM-25533][BEEEP] Refactor license date calculations into extensions (#6295) * Refactor license date calculations into extensions * `dotnet format` * Handling case when expirationWithoutGracePeriod is null * Removed extra UseAdminSponsoredFamilies claim --- .../Licenses/Extensions/LicenseExtensions.cs | 103 ++++++++++++------ .../OrganizationLicenseClaimsFactory.cs | 25 ++--- .../Models/OrganizationLicense.cs | 49 +-------- 3 files changed, 86 insertions(+), 91 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index f5b4499ea8..8cd3438191 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -9,75 +9,108 @@ namespace Bit.Core.Billing.Licenses.Extensions; public static class LicenseExtensions { - public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo) + public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime issued) { + if (subscriptionInfo?.Subscription == null) { - if (org.ExpirationDate.HasValue) - { - return org.ExpirationDate.Value; - } - - return DateTime.UtcNow.AddDays(7); + // Subscription isn't setup yet, so fallback to the organization's expiration date + // If there isn't an expiration date on the org, treat it as a free trial + return org.ExpirationDate ?? issued.AddDays(7); } var subscription = subscriptionInfo.Subscription; if (subscription.TrialEndDate > DateTime.UtcNow) { + // Still trialing, use trial's end date return subscription.TrialEndDate.Value; } - if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) + if (org.ExpirationDate < DateTime.UtcNow) { + // Organization is expired return org.ExpirationDate.Value; } - if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180)) + if (subscription.PeriodDuration > TimeSpan.FromDays(180)) { + // Annual subscription - include grace period to give the administrators time to upload a new license return subscription.PeriodEndDate - .Value - .AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays); + !.Value + .AddDays(Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays); } - return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1); + // Monthly subscription - giving an annual expiration to not burnden admins to upload fresh licenses each month + return org.ExpirationDate?.AddMonths(11) ?? issued.AddYears(1); } - public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) + public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime issued) { - if (subscriptionInfo?.Subscription == null || - subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow || - org.ExpirationDate < DateTime.UtcNow) - { - return expirationDate; - } - return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) || - DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30) - ? DateTime.UtcNow.AddDays(30) - : expirationDate; - } - - public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) - { - if (subscriptionInfo?.Subscription is null) + if (subscriptionInfo?.Subscription == null) { - return expirationDate; + // Subscription isn't setup yet, so fallback to the organization's expiration date + // If there isn't an expiration date on the org, treat it as a free trial + return org.ExpirationDate ?? issued.AddDays(7); } var subscription = subscriptionInfo.Subscription; - if (subscription.TrialEndDate <= DateTime.UtcNow && - org.ExpirationDate >= DateTime.UtcNow && - subscription.PeriodEndDate.HasValue && - subscription.PeriodDuration > TimeSpan.FromDays(180)) + if (subscription.TrialEndDate > DateTime.UtcNow) { - return subscription.PeriodEndDate.Value; + // Still trialing, use trial's end date + return subscription.TrialEndDate.Value; } - return expirationDate; + if (org.ExpirationDate < DateTime.UtcNow) + { + // Organization is expired + return org.ExpirationDate.Value; + } + + if (subscription.PeriodDuration > TimeSpan.FromDays(180)) + { + // Annual subscription - refresh every 30 days to check for plan changes, cancellations, and payment issues + return issued.AddDays(30); + } + + var expires = org.ExpirationDate?.AddMonths(11) ?? issued.AddYears(1); + + // If expiration is more than 30 days in the past, refresh in 30 days instead of using the stale date to give + // them a chance to refresh. Otherwise, uses the expiration date + return issued - expires > TimeSpan.FromDays(30) + ? issued.AddDays(30) + : expires; } + public static DateTime? CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo) + { + // It doesn't make sense that this returns null sometimes. If the expiration date doesn't include a grace period + // then we should just return the expiration date instead of null. This is currently forcing the single consumer + // to check for nulls. + + // At some point in the future, we should update this. We can't easily, though, without breaking the signatures + // since `ExpirationWithoutGracePeriod` is included on them. So for now, I'll shake my fist and then move on. + + // Only set expiration without grace period for active, non-trial, annual subscriptions + if (subscriptionInfo?.Subscription != null && + subscriptionInfo.Subscription.TrialEndDate <= DateTime.UtcNow && + org.ExpirationDate >= DateTime.UtcNow && + subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) + { + return subscriptionInfo.Subscription.PeriodEndDate; + } + + // Otherwise, return null. + return null; + } + + public static bool CalculateIsTrialing(this Organization org, SubscriptionInfo subscriptionInfo) => + subscriptionInfo?.Subscription is null + ? !org.ExpirationDate.HasValue + : subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow; + public static T GetValue(this ClaimsPrincipal principal, string claimType) { var claim = principal.FindFirst(claimType); diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 02b35583af..1e049d7f03 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Licenses.Models; using Bit.Core.Enums; -using Bit.Core.Models.Business; namespace Bit.Core.Billing.Licenses.Services.Implementations; @@ -15,11 +14,12 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory> GenerateClaims(Organization entity, LicenseContext licenseContext) { + var issued = DateTime.UtcNow; var subscriptionInfo = licenseContext.SubscriptionInfo; - var expires = entity.CalculateFreshExpirationDate(subscriptionInfo); - var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires); - var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires); - var trial = IsTrialing(entity, subscriptionInfo); + var expires = entity.CalculateFreshExpirationDate(subscriptionInfo, issued); + var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, issued); + var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo); + var trial = entity.CalculateIsTrialing(subscriptionInfo); var claims = new List { @@ -50,10 +50,9 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory - subscriptionInfo?.Subscription is null - ? !org.ExpirationDate.HasValue - : subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow; } diff --git a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs index 54e20cd636..83789be2f3 100644 --- a/src/Core/Billing/Organizations/Models/OrganizationLicense.cs +++ b/src/Core/Billing/Organizations/Models/OrganizationLicense.cs @@ -96,50 +96,13 @@ public class OrganizationLicense : ILicense AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems; // - if (subscriptionInfo?.Subscription == null) - { - if (org.ExpirationDate.HasValue) - { - Expires = Refresh = org.ExpirationDate.Value; - Trial = false; - } - else - { - Expires = Refresh = Issued.AddDays(7); - Trial = true; - } - } - else if (subscriptionInfo.Subscription.TrialEndDate.HasValue && - subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) - { - Expires = Refresh = subscriptionInfo.Subscription.TrialEndDate.Value; - Trial = true; - } - else - { - if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) - { - // expired - Expires = Refresh = org.ExpirationDate.Value; - } - else if (subscriptionInfo?.Subscription?.PeriodDuration != null && - subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) - { - Refresh = DateTime.UtcNow.AddDays(30); - Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Core.Constants - .OrganizationSelfHostSubscriptionGracePeriodDays); - ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate; - } - else - { - Expires = org.ExpirationDate.HasValue ? org.ExpirationDate.Value.AddMonths(11) : Issued.AddYears(1); - Refresh = DateTime.UtcNow - Expires > TimeSpan.FromDays(30) ? DateTime.UtcNow.AddDays(30) : Expires; - } - - Trial = false; - } - UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies; + + Expires = org.CalculateFreshExpirationDate(subscriptionInfo, Issued); + Refresh = org.CalculateFreshRefreshDate(subscriptionInfo, Issued); + ExpirationWithoutGracePeriod = org.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo); + Trial = org.CalculateIsTrialing(subscriptionInfo); + Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); } From a173e7e2da734517f283ecd9e8ab1b62be0d2349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 15 Sep 2025 18:05:06 +0200 Subject: [PATCH 241/326] [PM-25182] Improve Swagger OperationIDs for Vault (#6240) * Improve Swagger OperationIDs for Vault * Some renames --- .../Vault/Controllers/CiphersController.cs | 132 +++++++++++++++--- .../Vault/Controllers/FoldersController.cs | 18 ++- 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index db3d5fb357..6249e264c0 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -118,7 +118,6 @@ public class CiphersController : Controller return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); } - [HttpGet("{id}/full-details")] [HttpGet("{id}/details")] public async Task GetDetails(Guid id) { @@ -134,8 +133,15 @@ public class CiphersController : Controller return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers); } + [HttpGet("{id}/full-details")] + [Obsolete("This endpoint is deprecated. Use GET details method instead.")] + public async Task GetFullDetails(Guid id) + { + return await GetDetails(id); + } + [HttpGet("")] - public async Task> Get() + public async Task> GetAll() { var user = await _userService.GetUserByPrincipalAsync(User); var hasOrgs = _currentContext.Organizations.Count != 0; @@ -242,7 +248,6 @@ public class CiphersController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(Guid id, [FromBody] CipherRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -283,8 +288,14 @@ public class CiphersController : Controller return response; } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostPut(Guid id, [FromBody] CipherRequestModel model) + { + return await Put(id, model); + } + [HttpPut("{id}/admin")] - [HttpPost("{id}/admin")] public async Task PutAdmin(Guid id, [FromBody] CipherRequestModel model) { var userId = _userService.GetProperUserId(User).Value; @@ -317,6 +328,13 @@ public class CiphersController : Controller return response; } + [HttpPost("{id}/admin")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostPutAdmin(Guid id, [FromBody] CipherRequestModel model) + { + return await PutAdmin(id, model); + } + [HttpGet("organization-details")] public async Task> GetOrganizationCiphers(Guid organizationId) { @@ -691,7 +709,6 @@ public class CiphersController : Controller } [HttpPut("{id}/partial")] - [HttpPost("{id}/partial")] public async Task PutPartial(Guid id, [FromBody] CipherPartialRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -707,8 +724,14 @@ public class CiphersController : Controller return response; } + [HttpPost("{id}/partial")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostPartial(Guid id, [FromBody] CipherPartialRequestModel model) + { + return await PutPartial(id, model); + } + [HttpPut("{id}/share")] - [HttpPost("{id}/share")] public async Task PutShare(Guid id, [FromBody] CipherShareRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -744,8 +767,14 @@ public class CiphersController : Controller return response; } + [HttpPost("{id}/share")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostShare(Guid id, [FromBody] CipherShareRequestModel model) + { + return await PutShare(id, model); + } + [HttpPut("{id}/collections")] - [HttpPost("{id}/collections")] public async Task PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -770,8 +799,14 @@ public class CiphersController : Controller collectionCiphers); } + [HttpPost("{id}/collections")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutCollections(id, model); + } + [HttpPut("{id}/collections_v2")] - [HttpPost("{id}/collections_v2")] public async Task PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); @@ -804,8 +839,14 @@ public class CiphersController : Controller return response; } + [HttpPost("{id}/collections_v2")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutCollections_vNext(id, model); + } + [HttpPut("{id}/collections-admin")] - [HttpPost("{id}/collections-admin")] public async Task PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) { var userId = _userService.GetProperUserId(User).Value; @@ -834,6 +875,13 @@ public class CiphersController : Controller return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp); } + [HttpPost("{id}/collections-admin")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model) + { + return await PutCollectionsAdmin(id, model); + } + [HttpPost("bulk-collections")] public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model) { @@ -895,7 +943,6 @@ public class CiphersController : Controller } [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(Guid id) { var userId = _userService.GetProperUserId(User).Value; @@ -908,8 +955,14 @@ public class CiphersController : Controller await _cipherService.DeleteAsync(cipher, userId); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDelete(Guid id) + { + await Delete(id); + } + [HttpDelete("{id}/admin")] - [HttpPost("{id}/delete-admin")] public async Task DeleteAdmin(Guid id) { var userId = _userService.GetProperUserId(User).Value; @@ -923,8 +976,14 @@ public class CiphersController : Controller await _cipherService.DeleteAsync(cipher, userId, true); } + [HttpPost("{id}/delete-admin")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDeleteAdmin(Guid id) + { + await DeleteAdmin(id); + } + [HttpDelete("")] - [HttpPost("delete")] public async Task DeleteMany([FromBody] CipherBulkDeleteRequestModel model) { if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) @@ -937,8 +996,14 @@ public class CiphersController : Controller await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId); } + [HttpPost("delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDeleteMany([FromBody] CipherBulkDeleteRequestModel model) + { + await DeleteMany(model); + } + [HttpDelete("admin")] - [HttpPost("delete-admin")] public async Task DeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model) { if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) @@ -964,6 +1029,13 @@ public class CiphersController : Controller await _cipherService.DeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true); } + [HttpPost("delete-admin")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model) + { + await DeleteManyAdmin(model); + } + [HttpPut("{id}/delete")] public async Task PutDelete(Guid id) { @@ -1145,7 +1217,6 @@ public class CiphersController : Controller } [HttpPut("move")] - [HttpPost("move")] public async Task MoveMany([FromBody] CipherBulkMoveRequestModel model) { if (!_globalSettings.SelfHosted && model.Ids.Count() > 500) @@ -1158,8 +1229,14 @@ public class CiphersController : Controller string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId); } + [HttpPost("move")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostMoveMany([FromBody] CipherBulkMoveRequestModel model) + { + await MoveMany(model); + } + [HttpPut("share")] - [HttpPost("share")] public async Task> PutShareMany([FromBody] CipherBulkShareRequestModel model) { var organizationId = new Guid(model.Ciphers.First().OrganizationId); @@ -1207,6 +1284,13 @@ public class CiphersController : Controller return new ListResponseModel(response); } + [HttpPost("share")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task> PostShareMany([FromBody] CipherBulkShareRequestModel model) + { + return await PutShareMany(model); + } + [HttpPost("purge")] public async Task PostPurge([FromBody] SecretVerificationRequestModel model, Guid? organizationId = null) { @@ -1325,7 +1409,7 @@ public class CiphersController : Controller [Obsolete("Deprecated Attachments API", false)] [RequestSizeLimit(Constants.FileSize101mb)] [DisableFormValueModelBinding] - public async Task PostAttachment(Guid id) + public async Task PostAttachmentV1(Guid id) { ValidateAttachment(); @@ -1419,7 +1503,6 @@ public class CiphersController : Controller } [HttpDelete("{id}/attachment/{attachmentId}")] - [HttpPost("{id}/attachment/{attachmentId}/delete")] public async Task DeleteAttachment(Guid id, string attachmentId) { var userId = _userService.GetProperUserId(User).Value; @@ -1432,8 +1515,14 @@ public class CiphersController : Controller return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false); } + [HttpPost("{id}/attachment/{attachmentId}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDeleteAttachment(Guid id, string attachmentId) + { + return await DeleteAttachment(id, attachmentId); + } + [HttpDelete("{id}/attachment/{attachmentId}/admin")] - [HttpPost("{id}/attachment/{attachmentId}/delete-admin")] public async Task DeleteAttachmentAdmin(Guid id, string attachmentId) { var userId = _userService.GetProperUserId(User).Value; @@ -1447,6 +1536,13 @@ public class CiphersController : Controller return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); } + [HttpPost("{id}/attachment/{attachmentId}/delete-admin")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDeleteAttachmentAdmin(Guid id, string attachmentId) + { + return await DeleteAttachmentAdmin(id, attachmentId); + } + [AllowAnonymous] [HttpPost("attachment/validate/azure")] public async Task AzureValidateFile() diff --git a/src/Api/Vault/Controllers/FoldersController.cs b/src/Api/Vault/Controllers/FoldersController.cs index 9da9e6a184..195931f60c 100644 --- a/src/Api/Vault/Controllers/FoldersController.cs +++ b/src/Api/Vault/Controllers/FoldersController.cs @@ -45,7 +45,7 @@ public class FoldersController : Controller } [HttpGet("")] - public async Task> Get() + public async Task> GetAll() { var userId = _userService.GetProperUserId(User).Value; var folders = await _folderRepository.GetManyByUserIdAsync(userId); @@ -63,7 +63,6 @@ public class FoldersController : Controller } [HttpPut("{id}")] - [HttpPost("{id}")] public async Task Put(string id, [FromBody] FolderRequestModel model) { var userId = _userService.GetProperUserId(User).Value; @@ -77,8 +76,14 @@ public class FoldersController : Controller return new FolderResponseModel(folder); } + [HttpPost("{id}")] + [Obsolete("This endpoint is deprecated. Use PUT method instead.")] + public async Task PostPut(string id, [FromBody] FolderRequestModel model) + { + return await Put(id, model); + } + [HttpDelete("{id}")] - [HttpPost("{id}/delete")] public async Task Delete(string id) { var userId = _userService.GetProperUserId(User).Value; @@ -91,6 +96,13 @@ public class FoldersController : Controller await _cipherService.DeleteFolderAsync(folder); } + [HttpPost("{id}/delete")] + [Obsolete("This endpoint is deprecated. Use DELETE method instead.")] + public async Task PostDelete(string id) + { + await Delete(id); + } + [HttpDelete("all")] public async Task DeleteAll() { From b9f58946a37904e995f38f8b5649906d97671a00 Mon Sep 17 00:00:00 2001 From: MtnBurrit0 <77340197+mimartin12@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:23:29 -0600 Subject: [PATCH 242/326] Fix load test scheduled default path (#6339) --- .github/workflows/load-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index c582e6ba00..9bc6da89e7 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -35,7 +35,7 @@ env: AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH # Specify defaults for scheduled runs TEST_ID: ${{ inputs.test-id || 'server-load-test' }} - K6_TEST_PATH: ${{ inputs.k6-test-path || 'test/load/*.js' }} + K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }} API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }} IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }} From 2dd89b488d7bfe567bd57c76f4755426e3bba8c6 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Mon, 15 Sep 2025 14:11:25 -0400 Subject: [PATCH 243/326] Remove archive date from create request (#6341) --- src/Api/Vault/Models/Request/CipherRequestModel.cs | 2 -- src/Core/Vault/Models/Data/CipherDetails.cs | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 467be6e356..7ba72cccb7 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -46,7 +46,6 @@ public class CipherRequestModel public CipherSecureNoteModel SecureNote { get; set; } public CipherSSHKeyModel SSHKey { get; set; } public DateTime? LastKnownRevisionDate { get; set; } = null; - public DateTime? ArchivedDate { get; set; } public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true) { @@ -100,7 +99,6 @@ public class CipherRequestModel existingCipher.Reprompt = Reprompt; existingCipher.Key = Key; - existingCipher.ArchivedDate = ArchivedDate; var hasAttachments2 = (Attachments2?.Count ?? 0) > 0; var hasAttachments = (Attachments?.Count ?? 0) > 0; diff --git a/src/Core/Vault/Models/Data/CipherDetails.cs b/src/Core/Vault/Models/Data/CipherDetails.cs index e0ece1efec..e55cfd8cff 100644 --- a/src/Core/Vault/Models/Data/CipherDetails.cs +++ b/src/Core/Vault/Models/Data/CipherDetails.cs @@ -25,6 +25,7 @@ public class CipherDetails : CipherOrganizationDetails CreationDate = cipher.CreationDate; RevisionDate = cipher.RevisionDate; DeletedDate = cipher.DeletedDate; + ArchivedDate = cipher.ArchivedDate; Reprompt = cipher.Reprompt; Key = cipher.Key; OrganizationUseTotp = cipher.OrganizationUseTotp; From 6c512f1bc24158d4bdf561a74496f5d2d20143ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Mon, 15 Sep 2025 20:57:13 +0100 Subject: [PATCH 244/326] Add mobile CXP feature flags (#6343) --- src/Core/Constants.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 17dae1255c..8c7e3ff832 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -206,6 +206,8 @@ public static class FeatureFlagKeys 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"; /* Platform Team */ public const string IpcChannelFramework = "ipc-channel-framework"; From 4b3ac2ea61869f1c4013f7875bcac711495878fc Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:00:07 -0500 Subject: [PATCH 245/326] chore: resolve merge conflict to delete dc user removal feature flag, refs PM-24596 (#6344) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8c7e3ff832..43bba121df 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -131,7 +131,6 @@ 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 DirectoryConnectorPreventUserRemoval = "pm-24592-directory-connector-prevent-user-removal"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; From da48603c186e7e75ba46f050c8b420586f3c7874 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 16 Sep 2025 11:16:00 -0400 Subject: [PATCH 246/326] Revert "Remove archive date from create request (#6341)" (#6346) This reverts commit 2dd89b488d7bfe567bd57c76f4755426e3bba8c6. --- src/Api/Vault/Models/Request/CipherRequestModel.cs | 2 ++ src/Core/Vault/Models/Data/CipherDetails.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 7ba72cccb7..467be6e356 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -46,6 +46,7 @@ public class CipherRequestModel public CipherSecureNoteModel SecureNote { get; set; } public CipherSSHKeyModel SSHKey { get; set; } public DateTime? LastKnownRevisionDate { get; set; } = null; + public DateTime? ArchivedDate { get; set; } public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true) { @@ -99,6 +100,7 @@ public class CipherRequestModel existingCipher.Reprompt = Reprompt; existingCipher.Key = Key; + existingCipher.ArchivedDate = ArchivedDate; var hasAttachments2 = (Attachments2?.Count ?? 0) > 0; var hasAttachments = (Attachments?.Count ?? 0) > 0; diff --git a/src/Core/Vault/Models/Data/CipherDetails.cs b/src/Core/Vault/Models/Data/CipherDetails.cs index e55cfd8cff..e0ece1efec 100644 --- a/src/Core/Vault/Models/Data/CipherDetails.cs +++ b/src/Core/Vault/Models/Data/CipherDetails.cs @@ -25,7 +25,6 @@ public class CipherDetails : CipherOrganizationDetails CreationDate = cipher.CreationDate; RevisionDate = cipher.RevisionDate; DeletedDate = cipher.DeletedDate; - ArchivedDate = cipher.ArchivedDate; Reprompt = cipher.Reprompt; Key = cipher.Key; OrganizationUseTotp = cipher.OrganizationUseTotp; From 6e309c6e0463cee627e468aaae50489114d6c948 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:00:32 -0700 Subject: [PATCH 247/326] fix cipher org details with collections task (#6342) --- .../Vault/Repositories/CipherRepository.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index c741495f8e..4904574eee 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -902,13 +902,17 @@ public class CipherRepository : Repository, ICipherRepository var dict = new Dictionary(); var tempCollections = new Dictionary>(); - await connection.QueryAsync( + await connection.QueryAsync< + CipherOrganizationDetails, + CollectionCipher, + CipherOrganizationDetailsWithCollections + >( $"[{Schema}].[CipherOrganizationDetails_ReadByOrganizationIdExcludingDefaultCollections]", (cipher, cc) => { if (!dict.TryGetValue(cipher.Id, out var details)) { - details = new CipherOrganizationDetailsWithCollections(cipher, /*dummy*/null); + details = new CipherOrganizationDetailsWithCollections(cipher, new Dictionary>()); dict.Add(cipher.Id, details); tempCollections[cipher.Id] = new List(); } @@ -925,7 +929,6 @@ public class CipherRepository : Repository, ICipherRepository commandType: CommandType.StoredProcedure ); - // now assign each List back to the array property in one shot foreach (var kv in dict) { kv.Value.CollectionIds = tempCollections[kv.Key].ToArray(); From 57f891f391c730aff2ff479766b1e5687a753cbc Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:01:23 -0400 Subject: [PATCH 248/326] feat(sso): [auth/pm-17719] Make SSO identifier errors consistent (#6345) * feat(sso-account-controller): Make SSO identifiers consistent - align all return messages from prevalidate. * feat(shared-resources): Make SSO identifiers consistent - remove unused string resources, add new consistent error message. * feat(sso-account-controller): Make SSO identifiers consistent - Add logging. --- .../src/Sso/Controllers/AccountController.cs | 42 ++++++++----------- src/Core/Resources/SharedResources.en.resx | 18 ++------ 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs index 30b0d168d0..98a581e8ca 100644 --- a/bitwarden_license/src/Sso/Controllers/AccountController.cs +++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs @@ -108,36 +108,32 @@ public class AccountController : Controller // Validate domain_hint provided if (string.IsNullOrWhiteSpace(domainHint)) { - return InvalidJson("NoOrganizationIdentifierProvidedError"); + _logger.LogError(new ArgumentException("domainHint is required."), "domainHint not specified."); + return InvalidJson("SsoInvalidIdentifierError"); } // Validate organization exists from domain_hint var organization = await _organizationRepository.GetByIdentifierAsync(domainHint); - if (organization == null) + if (organization is not { UseSso: true }) { - return InvalidJson("OrganizationNotFoundByIdentifierError"); - } - if (!organization.UseSso) - { - return InvalidJson("SsoNotAllowedForOrganizationError"); + _logger.LogError("Organization not configured to use SSO."); + return InvalidJson("SsoInvalidIdentifierError"); } // Validate SsoConfig exists and is Enabled var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint); - if (ssoConfig == null) + if (ssoConfig is not { Enabled: true }) { - return InvalidJson("SsoConfigurationNotFoundForOrganizationError"); - } - if (!ssoConfig.Enabled) - { - return InvalidJson("SsoNotEnabledForOrganizationError"); + _logger.LogError("SsoConfig not enabled."); + return InvalidJson("SsoInvalidIdentifierError"); } // Validate Authentication Scheme exists and is loaded (cache) var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString()); - if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme)) + if (scheme is not IDynamicAuthenticationScheme dynamicScheme) { - return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError"); + _logger.LogError("Invalid authentication scheme for organization."); + return InvalidJson("SsoInvalidIdentifierError"); } // Run scheme validation @@ -147,13 +143,8 @@ public class AccountController : Controller } catch (Exception ex) { - var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message); - var errorKey = "InvalidSchemeConfigurationError"; - if (!translatedException.ResourceNotFound) - { - errorKey = ex.Message; - } - return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null); + _logger.LogError(ex, "An error occurred while validating SSO dynamic scheme."); + return InvalidJson("SsoInvalidIdentifierError"); } var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds); @@ -163,7 +154,8 @@ public class AccountController : Controller } catch (Exception ex) { - return InvalidJson("PreValidationError", ex); + _logger.LogError(ex, "An error occurred during SSO prevalidation."); + return InvalidJson("SsoInvalidIdentifierError"); } } @@ -352,7 +344,7 @@ public class AccountController : Controller } /// - /// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`. + /// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`. /// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records. /// private async Task<(User user, string provider, string providerUserId, IEnumerable claims, SsoConfigurationData config)> @@ -485,7 +477,7 @@ public class AccountController : Controller allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]); - // Since we're in the auto-provisioning logic, this means that the user exists, but they have not + // Since we're in the auto-provisioning logic, this means that the user exists, but they have not // authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them). // We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed // with authentication. diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 97cac5a610..17b4489454 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -394,24 +394,9 @@ The configured authentication scheme is not valid: "{0}" - - No scheme or handler for this SSO configuration found. - - - SSO is not yet enabled for this organization. - - - No SSO configuration exists for this organization. - - - SSO is not allowed for this organization. - Organization not found from identifier. - - No organization identifier provided. - Invalid authentication options provided to SAML2 scheme. @@ -691,4 +676,7 @@ Single sign on redirect token is invalid or expired. + + Invalid SSO identifier + From d83395aeb07226d5fb3df9ef14602eeab874392e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 17 Sep 2025 17:43:27 +0100 Subject: [PATCH 249/326] [PM-25372] Filter out DefaultUserCollections from CiphersController.GetAssignedOrganizationCiphers (#6274) Co-authored-by: Jimmy Vo --- .../ICollectionCipherRepository.cs | 1 + .../Vault/Queries/OrganizationCiphersQuery.cs | 2 +- .../CollectionCipherRepository.cs | 13 +++ .../CollectionCipherRepository.cs | 15 ++++ ...ctionCipher_ReadSharedByOrganizationId.sql | 17 ++++ src/Sql/dbo/Tables/Collection.sql | 2 +- .../CollectionCipherRepositoryTests.cs | 84 +++++++++++++++++++ ...llectionCipherManySharedByOrganization.sql | 30 +++++++ 8 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/CollectionCipher_ReadSharedByOrganizationId.sql create mode 100644 test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionCipherRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2025-09-03_00_CollectionCipherManySharedByOrganization.sql diff --git a/src/Core/Repositories/ICollectionCipherRepository.cs b/src/Core/Repositories/ICollectionCipherRepository.cs index 9494fec0ec..f7a4081b73 100644 --- a/src/Core/Repositories/ICollectionCipherRepository.cs +++ b/src/Core/Repositories/ICollectionCipherRepository.cs @@ -8,6 +8,7 @@ public interface ICollectionCipherRepository { Task> GetManyByUserIdAsync(Guid userId); Task> GetManyByOrganizationIdAsync(Guid organizationId); + Task> GetManySharedByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId); Task UpdateCollectionsAsync(Guid cipherId, Guid userId, IEnumerable collectionIds); Task UpdateCollectionsForAdminAsync(Guid cipherId, Guid organizationId, IEnumerable collectionIds); diff --git a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs index 945fdb7e3c..62b055b417 100644 --- a/src/Core/Vault/Queries/OrganizationCiphersQuery.cs +++ b/src/Core/Vault/Queries/OrganizationCiphersQuery.cs @@ -24,7 +24,7 @@ public class OrganizationCiphersQuery : IOrganizationCiphersQuery var orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId).ToList(); var orgCipherIds = orgCiphers.Select(c => c.Id); - var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId); + var collectionCiphers = await _collectionCipherRepository.GetManySharedByOrganizationIdAsync(organizationId); var collectionCiphersGroupDict = collectionCiphers .Where(c => orgCipherIds.Contains(c.CipherId)) .GroupBy(c => c.CipherId).ToDictionary(s => s.Key); diff --git a/src/Infrastructure.Dapper/Repositories/CollectionCipherRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionCipherRepository.cs index 5ed82a9a2c..64b1a74072 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionCipherRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionCipherRepository.cs @@ -45,6 +45,19 @@ public class CollectionCipherRepository : BaseRepository, ICollectionCipherRepos } } + public async Task> GetManySharedByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[CollectionCipher_ReadSharedByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs index d0787f7303..6e2805f987 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs @@ -47,6 +47,21 @@ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollec } } + public async Task> GetManySharedByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var data = await (from cc in dbContext.CollectionCiphers + join c in dbContext.Collections + on cc.CollectionId equals c.Id + where c.OrganizationId == organizationId + && c.Type == Core.Enums.CollectionType.SharedCollection + select cc).ToArrayAsync(); + return data; + } + } + public async Task> GetManyByUserIdAsync(Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadSharedByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadSharedByOrganizationId.sql new file mode 100644 index 0000000000..d35dabb0e4 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CollectionCipher_ReadSharedByOrganizationId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[CollectionCipher_ReadSharedByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + CC.[CollectionId], + CC.[CipherId] + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CC.[CollectionId] + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[Type] = 0 -- SharedCollections only +END diff --git a/src/Sql/dbo/Tables/Collection.sql b/src/Sql/dbo/Tables/Collection.sql index 03064fd978..2f0d3b943b 100644 --- a/src/Sql/dbo/Tables/Collection.sql +++ b/src/Sql/dbo/Tables/Collection.sql @@ -14,6 +14,6 @@ GO CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll] ON [dbo].[Collection]([OrganizationId] ASC) - INCLUDE([CreationDate], [Name], [RevisionDate]); + INCLUDE([CreationDate], [Name], [RevisionDate], [Type]); GO diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionCipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionCipherRepositoryTests.cs new file mode 100644 index 0000000000..1579e5c329 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionCipherRepositoryTests.cs @@ -0,0 +1,84 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories; + +public class CollectionCipherRepositoryTests +{ + [Theory, DatabaseData] + public async Task GetManySharedByOrganizationIdAsync_OnlyReturnsSharedCollections( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + ICipherRepository cipherRepository, + ICollectionCipherRepository collectionCipherRepository) + { + // Arrange + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Enterprise", + BillingEmail = "billing@example.com" + }); + + var sharedCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Shared Collection", + OrganizationId = organization.Id, + Type = CollectionType.SharedCollection + }); + + var defaultUserCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Default User Collection", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }); + + var sharedCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var defaultCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + await collectionCipherRepository.AddCollectionsForManyCiphersAsync( + organization.Id, + new[] { sharedCipher.Id }, + new[] { sharedCollection.Id }); + + await collectionCipherRepository.AddCollectionsForManyCiphersAsync( + organization.Id, + new[] { defaultCipher.Id }, + new[] { defaultUserCollection.Id }); + + // Act + var result = await collectionCipherRepository.GetManySharedByOrganizationIdAsync(organization.Id); + + // Assert + Assert.Single(result); + Assert.Equal(sharedCollection.Id, result.First().CollectionId); + Assert.DoesNotContain(result, cc => cc.CollectionId == defaultUserCollection.Id); + + // Cleanup + await cipherRepository.DeleteAsync(sharedCipher); + await cipherRepository.DeleteAsync(defaultCipher); + await collectionRepository.DeleteAsync(sharedCollection); + await collectionRepository.DeleteAsync(defaultUserCollection); + await organizationRepository.DeleteAsync(organization); + } +} diff --git a/util/Migrator/DbScripts/2025-09-03_00_CollectionCipherManySharedByOrganization.sql b/util/Migrator/DbScripts/2025-09-03_00_CollectionCipherManySharedByOrganization.sql new file mode 100644 index 0000000000..d29856ca00 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-03_00_CollectionCipherManySharedByOrganization.sql @@ -0,0 +1,30 @@ +CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_ReadSharedByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + CC.[CollectionId], + CC.[CipherId] + FROM + [dbo].[CollectionCipher] CC + INNER JOIN + [dbo].[Collection] C ON C.[Id] = CC.[CollectionId] + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[Type] = 0 -- SharedCollections only +END +GO + +-- Update [IX_Collection_OrganizationId_IncludeAll] index to include [Type] column +IF EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_Collection_OrganizationId_IncludeAll' AND object_id = OBJECT_ID('[dbo].[Collection]')) +BEGIN + DROP INDEX [IX_Collection_OrganizationId_IncludeAll] ON [dbo].[Collection] +END +GO + +CREATE NONCLUSTERED INDEX [IX_Collection_OrganizationId_IncludeAll] + ON [dbo].[Collection]([OrganizationId] ASC) + INCLUDE([CreationDate], [Name], [RevisionDate], [Type]) +GO From 26e574e8d75f0dda47bfcb9ae12b11783096ade1 Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 17 Sep 2025 17:14:00 -0400 Subject: [PATCH 250/326] Auth/pm 25453/support n users auth request (#6347) * fix(pending-auth-request-view): [PM-25453] Bugfix Auth Requests Multiple Users Same Device - fixed view to allow for multiple users for each device when partitioning for the auth request view. --- .../Views/AuthRequestPendingDetailsView.sql | 2 +- ...00_UpdateAuthRequestPendingDetailsView.sql | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 util/Migrator/DbScripts/2025-09-16_00_UpdateAuthRequestPendingDetailsView.sql diff --git a/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql b/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql index d0433bca09..16f8a51195 100644 --- a/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql +++ b/src/Sql/dbo/Auth/Views/AuthRequestPendingDetailsView.sql @@ -7,7 +7,7 @@ AS SELECT [AR].*, [D].[Id] AS [DeviceId], - ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier] ORDER BY [AR].[CreationDate] DESC) AS [rn] + ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier], [AR].[UserId] ORDER BY [AR].[CreationDate] DESC) AS [rn] FROM [dbo].[AuthRequest] [AR] LEFT JOIN [dbo].[Device] [D] ON [AR].[RequestDeviceIdentifier] = [D].[Identifier] diff --git a/util/Migrator/DbScripts/2025-09-16_00_UpdateAuthRequestPendingDetailsView.sql b/util/Migrator/DbScripts/2025-09-16_00_UpdateAuthRequestPendingDetailsView.sql new file mode 100644 index 0000000000..21b51387af --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-16_00_UpdateAuthRequestPendingDetailsView.sql @@ -0,0 +1,42 @@ +CREATE OR ALTER VIEW [dbo].[AuthRequestPendingDetailsView] +AS + WITH + PendingRequests + AS + ( + SELECT + [AR].*, + [D].[Id] AS [DeviceId], + ROW_NUMBER() OVER (PARTITION BY [AR].[RequestDeviceIdentifier], [AR].[UserId] ORDER BY [AR].[CreationDate] DESC) AS [rn] + FROM [dbo].[AuthRequest] [AR] + LEFT JOIN [dbo].[Device] [D] + ON [AR].[RequestDeviceIdentifier] = [D].[Identifier] + AND [D].[UserId] = [AR].[UserId] + WHERE [AR].[Type] IN (0, 1) -- 0 = AuthenticateAndUnlock, 1 = Unlock + ) +SELECT + [PR].[Id], + [PR].[UserId], + [PR].[OrganizationId], + [PR].[Type], + [PR].[RequestDeviceIdentifier], + [PR].[RequestDeviceType], + [PR].[RequestIpAddress], + [PR].[RequestCountryName], + [PR].[ResponseDeviceId], + [PR].[AccessCode], + [PR].[PublicKey], + [PR].[Key], + [PR].[MasterPasswordHash], + [PR].[Approved], + [PR].[CreationDate], + [PR].[ResponseDate], + [PR].[AuthenticationDate], + [PR].[DeviceId] +FROM [PendingRequests] [PR] +WHERE [PR].[rn] = 1 + AND [PR].[Approved] IS NULL -- since we only want pending requests we only want the most recent that is also approved = null +GO + +EXECUTE sp_refreshsqlmodule N'[dbo].[AuthRequestPendingDetailsView]' +GO From e46365ac206be26c393c8d0198bbd8aa8c5e24c7 Mon Sep 17 00:00:00 2001 From: Ben Brooks <56796209+bensbits91@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:36:00 -0700 Subject: [PATCH 251/326] feat(policies): add URI Match Defaults organizational policy (#6294) * feat(policies): add URI Match Defaults organizational policy Signed-off-by: Ben Brooks * feat(policies): remove unecessary model and org feature Signed-off-by: Ben Brooks --------- Signed-off-by: Ben Brooks --- src/Core/AdminConsole/Enums/PolicyType.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index ab39e543f8..f038faaf0b 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -18,6 +18,7 @@ public enum PolicyType : byte FreeFamiliesSponsorshipPolicy = 13, RemoveUnlockWithPin = 14, RestrictedItemTypesPolicy = 15, + UriMatchDefaults = 16, } public static class PolicyTypeExtensions @@ -46,6 +47,7 @@ public static class PolicyTypeExtensions PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship", PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN", PolicyType.RestrictedItemTypesPolicy => "Restricted item types", + PolicyType.UriMatchDefaults => "URI match defaults", }; } } From 780400fcf9ed8a7f4650e01ac825a9a480e49121 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:50:36 +1000 Subject: [PATCH 252/326] [PM-25138] Reduce db locking when creating default collections (#6308) * Use single method for default collection creation * Use GenerateComb to create sequential guids * Pre-sort data for SqlBulkCopy * Add SqlBulkCopy options per dbops recommendations --- .../ConfirmOrganizationUserCommand.cs | 62 ++++++++++++++++--- ...anizationDataOwnershipPolicyRequirement.cs | 5 ++ .../Repositories/ICollectionRepository.cs | 9 ++- src/Core/Utilities/CoreHelpers.cs | 7 ++- .../Helpers/BulkResourceCreationService.cs | 33 ++++++++-- .../Repositories/CollectionRepository.cs | 10 +-- .../Utilities/SqlGuidHelpers.cs | 26 ++++++++ .../Repositories/CollectionRepository.cs | 11 ++-- .../ConfirmOrganizationUserCommandTests.cs | 42 ++++++++++--- 9 files changed, 172 insertions(+), 33 deletions(-) create mode 100644 src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 83ec244c47..2fbe6be5c6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -82,7 +83,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand throw new BadRequestException(error); } - await HandleConfirmationSideEffectsAsync(organizationId, confirmedOrganizationUsers: [orgUser], defaultUserCollectionName); + await CreateDefaultCollectionAsync(orgUser, defaultUserCollectionName); return orgUser; } @@ -97,9 +98,13 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand .Select(r => r.Item1) .ToList(); - if (confirmedOrganizationUsers.Count > 0) + if (confirmedOrganizationUsers.Count == 1) { - await HandleConfirmationSideEffectsAsync(organizationId, confirmedOrganizationUsers, defaultUserCollectionName); + await CreateDefaultCollectionAsync(confirmedOrganizationUsers.Single(), defaultUserCollectionName); + } + else if (confirmedOrganizationUsers.Count > 1) + { + await CreateManyDefaultCollectionsAsync(organizationId, confirmedOrganizationUsers, defaultUserCollectionName); } return result; @@ -245,14 +250,54 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } /// - /// Handles the side effects of confirming an organization user. - /// Creates a default collection for the user if the organization - /// has the OrganizationDataOwnership policy enabled. + /// Creates a default collection for a single user if required by the Organization Data Ownership policy. + /// + /// The organization user who has just been confirmed. + /// The encrypted default user collection name. + private async Task CreateDefaultCollectionAsync(OrganizationUser organizationUser, string defaultUserCollectionName) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) + { + return; + } + + // Skip if no collection name provided (backwards compatibility) + if (string.IsNullOrWhiteSpace(defaultUserCollectionName)) + { + return; + } + + var organizationDataOwnershipPolicy = + await _policyRequirementQuery.GetAsync(organizationUser.UserId!.Value); + if (!organizationDataOwnershipPolicy.RequiresDefaultCollectionOnConfirm(organizationUser.OrganizationId)) + { + return; + } + + var defaultCollection = new Collection + { + OrganizationId = organizationUser.OrganizationId, + Name = defaultUserCollectionName, + Type = CollectionType.DefaultUserCollection + }; + var collectionUser = new CollectionAccessSelection + { + Id = organizationUser.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + }; + + await _collectionRepository.CreateAsync(defaultCollection, groups: null, users: [collectionUser]); + } + + /// + /// Creates default collections for multiple users if required by the Organization Data Ownership policy. /// /// The organization ID. /// The confirmed organization users. /// The encrypted default user collection name. - private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, + private async Task CreateManyDefaultCollectionsAsync(Guid organizationId, IEnumerable confirmedOrganizationUsers, string defaultUserCollectionName) { if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)) @@ -266,7 +311,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand return; } - var policyEligibleOrganizationUserIds = await _policyRequirementQuery.GetManyByOrganizationIdAsync(organizationId); + var policyEligibleOrganizationUserIds = + await _policyRequirementQuery.GetManyByOrganizationIdAsync(organizationId); var eligibleOrganizationUserIds = confirmedOrganizationUsers .Where(ou => policyEligibleOrganizationUserIds.Contains(ou.Id)) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs index cb72a51850..28d6614dcb 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/OrganizationDataOwnershipPolicyRequirement.cs @@ -67,6 +67,11 @@ public class OrganizationDataOwnershipPolicyRequirement : IPolicyRequirement var noCollectionNeeded = new DefaultCollectionRequest(Guid.Empty, false); return noCollectionNeeded; } + + public bool RequiresDefaultCollectionOnConfirm(Guid organizationId) + { + return _policyDetails.Any(p => p.OrganizationId == organizationId); + } } public record DefaultCollectionRequest(Guid OrganizationUserId, bool ShouldCreateDefaultCollection) diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index ca3e52751c..f86147ca7d 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -63,5 +63,12 @@ public interface ICollectionRepository : IRepository Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable collectionIds, IEnumerable users, IEnumerable groups); - Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName); + /// + /// Creates default user collections for the specified organization users if they do not already have one. + /// + /// The Organization ID. + /// The Organization User IDs to create default collections for. + /// The encrypted string to use as the default collection name. + /// + Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName); } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 813eb6d1aa..5acdc63489 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -41,9 +41,12 @@ public static class CoreHelpers }; /// - /// Generate sequential Guid for Sql Server. - /// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs + /// Generate a sequential Guid for Sql Server. This prevents SQL Server index fragmentation by incorporating timestamp + /// information for sequential ordering. This should be preferred to for any database IDs. /// + /// + /// ref: https://github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/Id/GuidCombGenerator.cs + /// /// A comb Guid. public static Guid GenerateComb() => GenerateComb(Guid.NewGuid(), DateTime.UtcNow); diff --git a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs index 3610c1c484..5a743ba028 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Helpers/BulkResourceCreationService.cs @@ -1,6 +1,7 @@ using System.Data; using Bit.Core.Entities; using Bit.Core.Vault.Entities; +using Bit.Infrastructure.Dapper.Utilities; using Microsoft.Data.SqlClient; namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers; @@ -8,11 +9,25 @@ namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers; public static class BulkResourceCreationService { private const string _defaultErrorMessage = "Must have at least one record for bulk creation."; - public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable collectionUsers, string errorMessage = _defaultErrorMessage) + public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction, + IEnumerable collectionUsers, string errorMessage = _defaultErrorMessage) { + // Offload some work from SQL Server by pre-sorting before insert. + // This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks. + var sortedCollectionUsers = collectionUsers + .OrderBySqlGuid(cu => cu.CollectionId) + .ThenBySqlGuid(cu => cu.OrganizationUserId) + .ToList(); + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); bulkCopy.DestinationTableName = "[dbo].[CollectionUser]"; - var dataTable = BuildCollectionsUsersTable(bulkCopy, collectionUsers, errorMessage); + bulkCopy.BatchSize = 500; + bulkCopy.BulkCopyTimeout = 120; + bulkCopy.EnableStreaming = true; + bulkCopy.ColumnOrderHints.Add("CollectionId", SortOrder.Ascending); + bulkCopy.ColumnOrderHints.Add("OrganizationUserId", SortOrder.Ascending); + + var dataTable = BuildCollectionsUsersTable(bulkCopy, sortedCollectionUsers, errorMessage); await bulkCopy.WriteToServerAsync(dataTable); } @@ -96,11 +111,21 @@ public static class BulkResourceCreationService return table; } - public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable collections, string errorMessage = _defaultErrorMessage) + public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction, + IEnumerable collections, string errorMessage = _defaultErrorMessage) { + // Offload some work from SQL Server by pre-sorting before insert. + // This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks. + var sortedCollections = collections.OrderBySqlGuid(c => c.Id).ToList(); + using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction); bulkCopy.DestinationTableName = "[dbo].[Collection]"; - var dataTable = BuildCollectionsTable(bulkCopy, collections, errorMessage); + bulkCopy.BatchSize = 500; + bulkCopy.BulkCopyTimeout = 120; + bulkCopy.EnableStreaming = true; + bulkCopy.ColumnOrderHints.Add("Id", SortOrder.Ascending); + + var dataTable = BuildCollectionsTable(bulkCopy, sortedCollections, errorMessage); await bulkCopy.WriteToServerAsync(dataTable); } diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index ad00ac7086..c2a59f75aa 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -6,6 +6,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.AdminConsole.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -326,9 +327,10 @@ public class CollectionRepository : Repository, ICollectionRep } } - public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable affectedOrgUserIds, string defaultCollectionName) + public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName) { - if (!affectedOrgUserIds.Any()) + organizationUserIds = organizationUserIds.ToList(); + if (!organizationUserIds.Any()) { return; } @@ -340,7 +342,7 @@ public class CollectionRepository : Repository, ICollectionRep { var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(connection, transaction, organizationId); - var missingDefaultCollectionUserIds = affectedOrgUserIds.Except(orgUserIdWithDefaultCollection); + var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection); var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName); @@ -393,7 +395,7 @@ public class CollectionRepository : Repository, ICollectionRep foreach (var orgUserId in missingDefaultCollectionUserIds) { - var collectionId = Guid.NewGuid(); + var collectionId = CoreHelpers.GenerateComb(); collections.Add(new Collection { diff --git a/src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs b/src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs new file mode 100644 index 0000000000..fc548e2ff0 --- /dev/null +++ b/src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs @@ -0,0 +1,26 @@ +using System.Data.SqlTypes; + +namespace Bit.Infrastructure.Dapper.Utilities; + +public static class SqlGuidHelpers +{ + /// + /// Sorts the source IEnumerable by the specified Guid property using the comparison logic. + /// This is required because MSSQL server compares (and therefore sorts) Guids differently to C#. + /// Ref: https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/compare-guid-uniqueidentifier-values + /// + public static IOrderedEnumerable OrderBySqlGuid( + this IEnumerable source, + Func keySelector) + { + return source.OrderBy(x => new SqlGuid(keySelector(x))); + } + + /// + public static IOrderedEnumerable ThenBySqlGuid( + this IOrderedEnumerable source, + Func keySelector) + { + return source.ThenBy(x => new SqlGuid(keySelector(x))); + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 021b5bcf16..5aa156d1f8 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -2,6 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories.Queries; using LinqToDB.EntityFrameworkCore; @@ -793,9 +794,10 @@ public class CollectionRepository : Repository affectedOrgUserIds, string defaultCollectionName) + public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable organizationUserIds, string defaultCollectionName) { - if (!affectedOrgUserIds.Any()) + organizationUserIds = organizationUserIds.ToList(); + if (!organizationUserIds.Any()) { return; } @@ -804,8 +806,7 @@ public class CollectionRepository : Repository().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); + var policyDetails = new PolicyDetails + { + OrganizationId = organization.Id, + OrganizationUserId = orgUser.Id, + IsProvider = false, + OrganizationUserStatus = orgUser.Status, + OrganizationUserType = orgUser.Type, + PolicyType = PolicyType.OrganizationDataOwnership + }; sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(organization.Id) - .Returns(new List { orgUser.Id }); + .GetAsync(orgUser.UserId!.Value) + .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Enabled, [policyDetails])); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); await sutProvider.GetDependency() .Received(1) - .UpsertDefaultCollectionsAsync( - organization.Id, - Arg.Is>(ids => ids.Contains(orgUser.Id)), - collectionName); + .CreateAsync( + Arg.Is(c => + c.Name == collectionName && + c.OrganizationId == organization.Id && + c.Type == CollectionType.DefaultUserCollection), + Arg.Any>(), + Arg.Is>(cu => + cu.Single().Id == orgUser.Id && + cu.Single().Manage)); } [Theory, BitAutoData] @@ -511,7 +526,7 @@ public class ConfirmOrganizationUserCommandTests [Theory, BitAutoData] public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithOrganizationDataOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection( Organization org, OrganizationUser confirmingUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + [OrganizationUser(OrganizationUserStatusType.Accepted, OrganizationUserType.Owner)] OrganizationUser orgUser, User user, string key, string collectionName, SutProvider sutProvider) { org.PlanType = PlanType.EnterpriseAnnually; @@ -523,9 +538,18 @@ public class ConfirmOrganizationUserCommandTests sutProvider.GetDependency().GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true); + var policyDetails = new PolicyDetails + { + OrganizationId = org.Id, + OrganizationUserId = orgUser.Id, + IsProvider = false, + OrganizationUserStatus = orgUser.Status, + OrganizationUserType = orgUser.Type, + PolicyType = PolicyType.OrganizationDataOwnership + }; sutProvider.GetDependency() - .GetManyByOrganizationIdAsync(org.Id) - .Returns(new List { orgUser.UserId!.Value }); + .GetAsync(orgUser.UserId!.Value) + .Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, [policyDetails])); await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName); From 866a572d26c842cbddeffab9cc0f3378db90ee80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Thu, 18 Sep 2025 13:41:19 +0200 Subject: [PATCH 253/326] Enable custom IDs for bindings (#6340) * Enable custom IDs for bindings * Remove description --- src/Api/Controllers/CollectionsController.cs | 2 +- src/Api/Controllers/DevicesController.cs | 2 +- src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs | 4 ---- src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs | 7 ++++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index f037ab7034..b3542cfde2 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -199,7 +199,7 @@ public class CollectionsController : Controller [HttpPost("{id}")] [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")] - public async Task Post(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model) + public async Task PostPut(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model) { return await Put(orgId, id, model); } diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index 1f2cda9cc4..d54b3c7b8c 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -115,7 +115,7 @@ public class DevicesController : Controller [HttpPost("{id}")] [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")] - public async Task Post(string id, [FromBody] DeviceRequestModel model) + public async Task PostPut(string id, [FromBody] DeviceRequestModel model) { return await Put(id, model); } diff --git a/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs index fba8b17078..68c0b5145a 100644 --- a/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs +++ b/src/SharedWeb/Swagger/SourceFileLineOperationFilter.cs @@ -23,10 +23,6 @@ public class SourceFileLineOperationFilter : IOperationFilter var (fileName, lineNumber) = GetSourceFileLine(context.MethodInfo); if (fileName != null && lineNumber > 0) { - // Add the information with a link to the source file at the end of the operation description - operation.Description += - $"\nThis operation is defined on: [`https://github.com/bitwarden/server/blob/main/{fileName}#L{lineNumber}`]"; - // Also add the information as extensions, so other tools can use it in the future operation.Extensions.Add("x-source-file", new OpenApiString(fileName)); operation.Extensions.Add("x-source-line", new OpenApiInteger(lineNumber)); diff --git a/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs b/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs index 60803705d6..d57ee72d48 100644 --- a/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs +++ b/src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs @@ -19,9 +19,10 @@ public static class SwaggerGenOptionsExt // Set the operation ID to the name of the controller followed by the name of the function. // Note that the "Controller" suffix for the controllers, and the "Async" suffix for the actions // are removed already, so we don't need to do that ourselves. - // TODO(Dani): This is disabled until we remove all the duplicate operation IDs. - // config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}"); - // config.DocumentFilter(); + config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}"); + // Because we're setting custom operation IDs, we need to ensure that we don't accidentally + // introduce duplicate IDs, which is against the OpenAPI specification and could lead to issues. + config.DocumentFilter(); // These two filters require debug symbols/git, so only add them in development mode if (environment.IsDevelopment()) From c93c3464732c93c9be593a3a55b032c029c4bd6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 13:50:24 +0200 Subject: [PATCH 254/326] [deps] Platform: Update LaunchDarkly.ServerSdk to 8.10.1 (#6210) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a7db90e892..e76af0f8ef 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -59,7 +59,7 @@ - + From 9d3d35e0bf3f6f15d7705ffa12069bc427a600e0 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 18 Sep 2025 11:22:22 -0500 Subject: [PATCH 255/326] removing status from org name. (#6350) --- src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index e43d5a72bd..6e5504c73a 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -31,7 +31,7 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel ? string.Empty : CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), TitleThird = orgInvitesInfo.IsFreeOrg ? string.Empty : " on Bitwarden and start securing your passwords!", - OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false) + orgUser.Status, + OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), Email = WebUtility.UrlEncode(orgUser.Email), OrganizationId = orgUser.OrganizationId.ToString(), OrganizationUserId = orgUser.Id.ToString(), From 7e4dac98378e80e7fd2f60a510c877092889c30f Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:08:47 -0500 Subject: [PATCH 256/326] chore: remove FF, references, and restructure code, refs PM-24373 (#6353) --- .../SendOrganizationInvitesCommand.cs | 6 +--- src/Core/Constants.cs | 1 - .../Models/Mail/OrganizationInvitesInfo.cs | 6 ---- .../Mail/OrganizationUserInvitedViewModel.cs | 34 ------------------- .../Implementations/HandlebarsMailService.cs | 26 +++++--------- 5 files changed, 10 insertions(+), 63 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs index 69b968d438..cd5066d11b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/SendOrganizationInvitesCommand.cs @@ -22,8 +22,7 @@ public class SendOrganizationInvitesCommand( IPolicyRepository policyRepository, IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory dataProtectorTokenFactory, - IMailService mailService, - IFeatureService featureService) : ISendOrganizationInvitesCommand + IMailService mailService) : ISendOrganizationInvitesCommand { public async Task SendInvitesAsync(SendInvitesRequest request) { @@ -72,15 +71,12 @@ public class SendOrganizationInvitesCommand( var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair); - var isSubjectFeatureEnabled = featureService.IsEnabled(FeatureFlagKeys.InviteEmailImprovements); - return new OrganizationInvitesInfo( organization, orgSsoEnabled, orgSsoLoginRequiredPolicyEnabled, orgUsersWithExpTokens, orgUserHasExistingUserDict, - isSubjectFeatureEnabled, initOrganization ); } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 43bba121df..025871440f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -135,7 +135,6 @@ public static class FeatureFlagKeys public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; - public const string InviteEmailImprovements = "pm-25644-update-join-organization-subject-line"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/Models/Mail/OrganizationInvitesInfo.cs b/src/Core/Models/Mail/OrganizationInvitesInfo.cs index c31e00c184..af53f23a8a 100644 --- a/src/Core/Models/Mail/OrganizationInvitesInfo.cs +++ b/src/Core/Models/Mail/OrganizationInvitesInfo.cs @@ -15,7 +15,6 @@ public class OrganizationInvitesInfo bool orgSsoLoginRequiredPolicyEnabled, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> orgUserTokenPairs, Dictionary orgUserHasExistingUserDict, - bool isSubjectFeatureEnabled = false, bool initOrganization = false ) { @@ -30,8 +29,6 @@ public class OrganizationInvitesInfo OrgUserTokenPairs = orgUserTokenPairs; OrgUserHasExistingUserDict = orgUserHasExistingUserDict; - - IsSubjectFeatureEnabled = isSubjectFeatureEnabled; } public string OrganizationName { get; } @@ -40,9 +37,6 @@ public class OrganizationInvitesInfo public bool OrgSsoEnabled { get; } public string OrgSsoIdentifier { get; } public bool OrgSsoLoginRequiredPolicyEnabled { get; } - - public bool IsSubjectFeatureEnabled { get; } - public IEnumerable<(OrganizationUser OrgUser, ExpiringToken Token)> OrgUserTokenPairs { get; } public Dictionary OrgUserHasExistingUserDict { get; } diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index 6e5504c73a..669887c4b6 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -20,40 +20,6 @@ public class OrganizationUserInvitedViewModel : BaseTitleContactUsMailModel OrganizationUser orgUser, ExpiringToken expiringToken, GlobalSettings globalSettings) - { - var freeOrgTitle = "A Bitwarden member invited you to an organization. Join now to start securing your passwords!"; - - return new OrganizationUserInvitedViewModel - { - TitleFirst = orgInvitesInfo.IsFreeOrg ? freeOrgTitle : "Join ", - TitleSecondBold = - orgInvitesInfo.IsFreeOrg - ? string.Empty - : CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), - TitleThird = orgInvitesInfo.IsFreeOrg ? string.Empty : " on Bitwarden and start securing your passwords!", - OrganizationName = CoreHelpers.SanitizeForEmail(orgInvitesInfo.OrganizationName, false), - Email = WebUtility.UrlEncode(orgUser.Email), - OrganizationId = orgUser.OrganizationId.ToString(), - OrganizationUserId = orgUser.Id.ToString(), - Token = WebUtility.UrlEncode(expiringToken.Token), - ExpirationDate = - $"{expiringToken.ExpirationDate.ToLongDateString()} {expiringToken.ExpirationDate.ToShortTimeString()} UTC", - OrganizationNameUrlEncoded = WebUtility.UrlEncode(orgInvitesInfo.OrganizationName), - WebVaultUrl = globalSettings.BaseServiceUri.VaultWithHash, - SiteName = globalSettings.SiteName, - InitOrganization = orgInvitesInfo.InitOrganization, - OrgSsoIdentifier = orgInvitesInfo.OrgSsoIdentifier, - OrgSsoEnabled = orgInvitesInfo.OrgSsoEnabled, - OrgSsoLoginRequiredPolicyEnabled = orgInvitesInfo.OrgSsoLoginRequiredPolicyEnabled, - OrgUserHasExistingUser = orgInvitesInfo.OrgUserHasExistingUserDict[orgUser.Id] - }; - } - - public static OrganizationUserInvitedViewModel CreateFromInviteInfo_v2( - OrganizationInvitesInfo orgInvitesInfo, - OrganizationUser orgUser, - ExpiringToken expiringToken, - GlobalSettings globalSettings) { const string freeOrgTitle = "A Bitwarden member invited you to an organization. " + "Join now to start securing your passwords!"; diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 89a613b7ed..9728c2e727 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -355,11 +355,8 @@ public class HandlebarsMailService : IMailService { Debug.Assert(orgUserTokenPair.OrgUser.Email is not null); - var orgUserInviteViewModel = orgInvitesInfo.IsSubjectFeatureEnabled - ? OrganizationUserInvitedViewModel.CreateFromInviteInfo_v2( - orgInvitesInfo, orgUserTokenPair.OrgUser, orgUserTokenPair.Token, _globalSettings) - : OrganizationUserInvitedViewModel.CreateFromInviteInfo(orgInvitesInfo, orgUserTokenPair.OrgUser, - orgUserTokenPair.Token, _globalSettings); + var orgUserInviteViewModel = OrganizationUserInvitedViewModel.CreateFromInviteInfo(orgInvitesInfo, orgUserTokenPair.OrgUser, + orgUserTokenPair.Token, _globalSettings); return CreateMessage(orgUserTokenPair.OrgUser.Email, orgUserInviteViewModel); }); @@ -369,20 +366,15 @@ public class HandlebarsMailService : IMailService MailQueueMessage CreateMessage(string email, OrganizationUserInvitedViewModel model) { - var subject = $"Join {model.OrganizationName}"; + ArgumentNullException.ThrowIfNull(model); - if (orgInvitesInfo.IsSubjectFeatureEnabled) + var subject = model! switch { - ArgumentNullException.ThrowIfNull(model); - - subject = model! switch - { - { IsFreeOrg: true, OrgUserHasExistingUser: true } => "You have been invited to a Bitwarden Organization", - { IsFreeOrg: true, OrgUserHasExistingUser: false } => "You have been invited to Bitwarden Password Manager", - { IsFreeOrg: false, OrgUserHasExistingUser: true } => $"{model.OrganizationName} invited you to their Bitwarden organization", - { IsFreeOrg: false, OrgUserHasExistingUser: false } => $"{model.OrganizationName} set up a Bitwarden account for you" - }; - } + { IsFreeOrg: true, OrgUserHasExistingUser: true } => "You have been invited to a Bitwarden Organization", + { IsFreeOrg: true, OrgUserHasExistingUser: false } => "You have been invited to Bitwarden Password Manager", + { IsFreeOrg: false, OrgUserHasExistingUser: true } => $"{model.OrganizationName} invited you to their Bitwarden organization", + { IsFreeOrg: false, OrgUserHasExistingUser: false } => $"{model.OrganizationName} set up a Bitwarden account for you" + }; var message = CreateDefaultMessage(subject, email); From d2c2ae5b4d694087d0873b553820808fdf05add9 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:30:05 -0700 Subject: [PATCH 257/326] fix(invalid-auth-request-approvals): Auth/[PM-3387] Better Error Handling for Invalid Auth Request Approval (#6264) If a user approves an invalid auth request, on the Requesting Device they currently they get stuck on the `LoginViaAuthRequestComponent` with a spinning wheel. This PR makes it so that when an Approving Device attempts to approve an invalid auth request, the Approving Device receives an error toast and the `UpdateAuthRequestAsync()` operation is blocked. --- .../Controllers/AuthRequestsController.cs | 30 +++++ .../AuthRequestsControllerTests.cs | 114 +++++++++++++++++- 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index c62b817905..e4a9027f20 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -102,7 +102,37 @@ public class AuthRequestsController( public async Task Put(Guid id, [FromBody] AuthRequestUpdateRequestModel model) { var userId = _userService.GetProperUserId(User).Value; + + // If the Approving Device is attempting to approve a request, validate the approval + if (model.RequestApproved == true) + { + await ValidateApprovalOfMostRecentAuthRequest(id, userId); + } + var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model); return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault); } + + private async Task ValidateApprovalOfMostRecentAuthRequest(Guid id, Guid userId) + { + // Get the current auth request to find the device identifier + var currentAuthRequest = await _authRequestService.GetAuthRequestAsync(id, userId); + if (currentAuthRequest == null) + { + throw new NotFoundException(); + } + + // Get all pending auth requests for this user (returns most recent per device) + var pendingRequests = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId); + + // Find the most recent request for the same device + var mostRecentForDevice = pendingRequests + .FirstOrDefault(pendingRequest => pendingRequest.RequestDeviceIdentifier == currentAuthRequest.RequestDeviceIdentifier); + + var isMostRecentRequestForDevice = mostRecentForDevice?.Id == id; + if (!isMostRecentRequestForDevice) + { + throw new BadRequestException("This request is no longer valid. Make sure to approve the most recent request."); + } + } } diff --git a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs index 1b8e7aba8e..fc7eb0d93b 100644 --- a/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AuthRequestsControllerTests.cs @@ -222,7 +222,7 @@ public class AuthRequestsControllerTests } [Theory, BitAutoData] - public async Task Put_ReturnsAuthRequest( + public async Task Put_WithRequestNotApproved_ReturnsAuthRequest( SutProvider sutProvider, User user, AuthRequestUpdateRequestModel requestModel, @@ -230,6 +230,7 @@ public class AuthRequestsControllerTests { // Arrange SetBaseServiceUri(sutProvider); + requestModel.RequestApproved = false; // Not an approval, so validation should be skipped sutProvider.GetDependency() .GetProperUserId(Arg.Any()) @@ -248,6 +249,117 @@ public class AuthRequestsControllerTests Assert.IsType(result); } + [Theory, BitAutoData] + public async Task Put_WithApprovedRequest_ValidatesAndReturnsAuthRequest( + SutProvider sutProvider, + User user, + AuthRequestUpdateRequestModel requestModel, + AuthRequest currentAuthRequest, + AuthRequest updatedAuthRequest, + List pendingRequests) + { + // Arrange + SetBaseServiceUri(sutProvider); + requestModel.RequestApproved = true; // Approval triggers validation + currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123"; + + // Setup pending requests - make the current request the most recent for its device + var mostRecentForDevice = new PendingAuthRequestDetails(currentAuthRequest, Guid.NewGuid()); + pendingRequests.Add(mostRecentForDevice); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + // Setup validation dependencies + sutProvider.GetDependency() + .GetAuthRequestAsync(currentAuthRequest.Id, user.Id) + .Returns(currentAuthRequest); + + sutProvider.GetDependency() + .GetManyPendingAuthRequestByUserId(user.Id) + .Returns(pendingRequests); + + sutProvider.GetDependency() + .UpdateAuthRequestAsync(currentAuthRequest.Id, user.Id, requestModel) + .Returns(updatedAuthRequest); + + // Act + var result = await sutProvider.Sut + .Put(currentAuthRequest.Id, requestModel); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task Put_WithApprovedRequest_CurrentAuthRequestNotFound_ThrowsNotFoundException( + SutProvider sutProvider, + User user, + AuthRequestUpdateRequestModel requestModel, + Guid authRequestId) + { + // Arrange + requestModel.RequestApproved = true; // Approval triggers validation + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + // Current auth request not found + sutProvider.GetDependency() + .GetAuthRequestAsync(authRequestId, user.Id) + .Returns((AuthRequest)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.Put(authRequestId, requestModel)); + } + + [Theory, BitAutoData] + public async Task Put_WithApprovedRequest_NotMostRecentForDevice_ThrowsBadRequestException( + SutProvider sutProvider, + User user, + AuthRequestUpdateRequestModel requestModel, + AuthRequest currentAuthRequest, + List pendingRequests) + { + // Arrange + requestModel.RequestApproved = true; // Approval triggers validation + currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123"; + + // Setup pending requests - make a different request the most recent for the same device + var differentAuthRequest = new AuthRequest + { + Id = Guid.NewGuid(), // Different ID than current request + RequestDeviceIdentifier = currentAuthRequest.RequestDeviceIdentifier, + UserId = user.Id, + Type = AuthRequestType.AuthenticateAndUnlock, + CreationDate = DateTime.UtcNow + }; + var mostRecentForDevice = new PendingAuthRequestDetails(differentAuthRequest, Guid.NewGuid()); + pendingRequests.Add(mostRecentForDevice); + + sutProvider.GetDependency() + .GetProperUserId(Arg.Any()) + .Returns(user.Id); + + sutProvider.GetDependency() + .GetAuthRequestAsync(currentAuthRequest.Id, user.Id) + .Returns(currentAuthRequest); + + sutProvider.GetDependency() + .GetManyPendingAuthRequestByUserId(user.Id) + .Returns(pendingRequests); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.Put(currentAuthRequest.Id, requestModel)); + + Assert.Equal("This request is no longer valid. Make sure to approve the most recent request.", exception.Message); + } + private void SetBaseServiceUri(SutProvider sutProvider) { sutProvider.GetDependency() From 14b307c15b6af5fc05a86110d3fea7b6ca548387 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 19 Sep 2025 10:26:22 -0500 Subject: [PATCH 258/326] [PM-25205] Don't respond with a tax ID warning for US customers (#6310) * Don't respond with a Tax ID warning for US customers * Only show provider tax ID warning for non-US based providers --- .../Queries/GetProviderWarningsQuery.cs | 8 +- .../Queries/GetProviderWarningsQueryTests.cs | 79 +++- .../Queries/GetOrganizationWarningsQuery.cs | 6 + .../GetOrganizationWarningsQueryTests.cs | 441 +++++++++++++++++- 4 files changed, 496 insertions(+), 38 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs index 9392c285e0..cc77797307 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs @@ -10,6 +10,7 @@ using Stripe.Tax; namespace Bit.Commercial.Core.Billing.Providers.Queries; +using static Bit.Core.Constants; using static StripeConstants; using SuspensionWarning = ProviderWarnings.SuspensionWarning; using TaxIdWarning = ProviderWarnings.TaxIdWarning; @@ -61,6 +62,11 @@ public class GetProviderWarningsQuery( Provider provider, Customer customer) { + if (customer.Address?.Country == CountryAbbreviations.UnitedStates) + { + return null; + } + if (!currentContext.ProviderProviderAdmin(provider.Id)) { return null; @@ -75,7 +81,7 @@ public class GetProviderWarningsQuery( .SelectMany(registrations => registrations.Data); // Find the matching registration for the customer - var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address.Country); + var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country); // If we're not registered in their country, we don't need a warning if (registration == null) diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs index f199c44924..a7f896ef7a 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs @@ -58,7 +58,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -90,7 +90,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -124,7 +124,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -158,7 +158,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -191,7 +191,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -219,7 +219,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -227,7 +227,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "CA" }] + Data = [new Registration { Country = "GB" }] }); var response = await sutProvider.Sut.Run(provider); @@ -252,7 +252,7 @@ public class GetProviderWarningsQueryTests Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -260,7 +260,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "US" }] + Data = [new Registration { Country = "CA" }] }); var response = await sutProvider.Sut.Run(provider); @@ -291,7 +291,7 @@ public class GetProviderWarningsQueryTests { Data = [new TaxId { Verification = null }] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -299,7 +299,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "US" }] + Data = [new Registration { Country = "CA" }] }); var response = await sutProvider.Sut.Run(provider); @@ -333,7 +333,7 @@ public class GetProviderWarningsQueryTests } }] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -341,7 +341,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "US" }] + Data = [new Registration { Country = "CA" }] }); var response = await sutProvider.Sut.Run(provider); @@ -378,7 +378,7 @@ public class GetProviderWarningsQueryTests } }] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -386,7 +386,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "US" }] + Data = [new Registration { Country = "CA" }] }); var response = await sutProvider.Sut.Run(provider); @@ -423,7 +423,7 @@ public class GetProviderWarningsQueryTests } }] }, - Address = new Address { Country = "US" } + Address = new Address { Country = "CA" } } }); @@ -431,7 +431,7 @@ public class GetProviderWarningsQueryTests sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) .Returns(new StripeList { - Data = [new Registration { Country = "US" }] + Data = [new Registration { Country = "CA" }] }); var response = await sutProvider.Sut.Run(provider); @@ -498,6 +498,44 @@ public class GetProviderWarningsQueryTests Status = SubscriptionStatus.Unpaid, CancelAt = cancelAt, Customer = new Customer + { + TaxIds = new StripeList { Data = [] }, + Address = new Address { Country = "CA" } + } + }); + + sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); + sutProvider.GetDependency().TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = [new Registration { Country = "CA" }] + }); + + var response = await sutProvider.Sut.Run(provider); + + Assert.True(response is + { + Suspension.Resolution: "add_payment_method", + TaxId.Type: "tax_id_missing" + }); + Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt); + } + + [Theory, BitAutoData] + public async Task Run_USCustomer_NoTaxIdWarning( + Provider provider, + SutProvider sutProvider) + { + provider.Enabled = true; + + sutProvider.GetDependency() + .GetSubscription(provider, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = SubscriptionStatus.Active, + Customer = new Customer { TaxIds = new StripeList { Data = [] }, Address = new Address { Country = "US" } @@ -513,11 +551,6 @@ public class GetProviderWarningsQueryTests var response = await sutProvider.Sut.Run(provider); - Assert.True(response is - { - Suspension.Resolution: "add_payment_method", - TaxId.Type: "tax_id_missing" - }); - Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt); + Assert.Null(response!.TaxId); } } diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index 312623ffa5..f33814f1cf 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -15,6 +15,7 @@ using Stripe.Tax; namespace Bit.Core.Billing.Organizations.Queries; +using static Core.Constants; using static StripeConstants; using FreeTrialWarning = OrganizationWarnings.FreeTrialWarning; using InactiveSubscriptionWarning = OrganizationWarnings.InactiveSubscriptionWarning; @@ -232,6 +233,11 @@ public class GetOrganizationWarningsQuery( Customer customer, Provider? provider) { + if (customer.Address?.Country == CountryAbbreviations.UnitedStates) + { + return null; + } + var productTier = organization.PlanType.GetProductTier(); // Only business tier customers can have tax IDs diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index eefda06149..5234d500d1 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -4,6 +4,7 @@ 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.Services; using Bit.Core.Context; @@ -13,11 +14,14 @@ using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using NSubstitute.ReturnsExtensions; using Stripe; +using Stripe.Tax; using Stripe.TestHelpers; using Xunit; namespace Bit.Core.Test.Billing.Organizations.Queries; +using static StripeConstants; + [SutProviderCustomize] public class GetOrganizationWarningsQueryTests { @@ -57,7 +61,7 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Trialing, + Status = SubscriptionStatus.Trialing, TrialEnd = now.AddDays(7), Customer = new Customer { @@ -95,7 +99,7 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Trialing, + Status = SubscriptionStatus.Trialing, TrialEnd = now.AddDays(7), Customer = new Customer { @@ -142,7 +146,7 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Unpaid, + Status = SubscriptionStatus.Unpaid, Customer = new Customer { InvoiceSettings = new CustomerInvoiceSettings(), @@ -170,7 +174,8 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Unpaid + Customer = new Customer(), + Status = SubscriptionStatus.Unpaid }); sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) @@ -197,7 +202,8 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Unpaid + Customer = new Customer(), + Status = SubscriptionStatus.Unpaid }); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); @@ -223,7 +229,8 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Canceled + Customer = new Customer(), + Status = SubscriptionStatus.Canceled }); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); @@ -249,7 +256,8 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - Status = StripeConstants.SubscriptionStatus.Unpaid + Customer = new Customer(), + Status = SubscriptionStatus.Unpaid }); sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(false); @@ -275,8 +283,9 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, - Status = StripeConstants.SubscriptionStatus.Active, + CollectionMethod = CollectionMethod.SendInvoice, + Customer = new Customer(), + Status = SubscriptionStatus.Active, CurrentPeriodEnd = now.AddDays(10), TestClock = new TestClock { @@ -313,11 +322,12 @@ public class GetOrganizationWarningsQueryTests )) .Returns(new Subscription { - CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, - Status = StripeConstants.SubscriptionStatus.Active, + CollectionMethod = CollectionMethod.SendInvoice, + Customer = new Customer(), + Status = SubscriptionStatus.Active, LatestInvoice = new Invoice { - Status = StripeConstants.InvoiceStatus.Open, + Status = InvoiceStatus.Open, DueDate = now.AddDays(30), Created = now }, @@ -360,8 +370,9 @@ public class GetOrganizationWarningsQueryTests .Returns(new Subscription { Id = subscriptionId, - CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, - Status = StripeConstants.SubscriptionStatus.PastDue, + CollectionMethod = CollectionMethod.SendInvoice, + Customer = new Customer(), + Status = SubscriptionStatus.PastDue, TestClock = new TestClock { FrozenTime = now @@ -390,4 +401,406 @@ public class GetOrganizationWarningsQueryTests Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate); } + + [Theory, BitAutoData] + public async Task Run_USCustomer_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "US" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_FreeCustomer_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.Free; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_NotOwner_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(false); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_HasProvider_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider()); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_NoRegistrationInCountry_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "GB" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdWarning_Missing( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List() }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "CA" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + TaxId.Type: "tax_id_missing" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdWarning_PendingVerification( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.EnterpriseAnnually; + + var taxId = new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Pending + } + }; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List { taxId } }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "CA" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + TaxId.Type: "tax_id_pending_verification" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_TaxIdWarning_FailedVerification( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var taxId = new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Unverified + } + }; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List { taxId } }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "CA" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + TaxId.Type: "tax_id_failed_verification" + }); + } + + [Theory, BitAutoData] + public async Task Run_VerifiedTaxId_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var taxId = new TaxId + { + Verification = new TaxIdVerification + { + Status = TaxIdVerificationStatus.Verified + } + }; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List { taxId } }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "CA" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } + + [Theory, BitAutoData] + public async Task Run_NullVerification_NoTaxIdWarning( + Organization organization, + SutProvider sutProvider) + { + organization.PlanType = PlanType.TeamsAnnually; + + var taxId = new TaxId + { + Verification = null + }; + + var subscription = new Subscription + { + Customer = new Customer + { + Address = new Address { Country = "CA" }, + TaxIds = new StripeList { Data = new List { taxId } }, + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + } + }; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .OrganizationOwner(organization.Id) + .Returns(true); + + sutProvider.GetDependency() + .TaxRegistrationsListAsync(Arg.Any()) + .Returns(new StripeList + { + Data = new List + { + new() { Country = "CA" } + } + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.Null(response.TaxId); + } } From 3ac3b8c8d984eaf5fee59be35bb2bd4cb0c8ac61 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:27:12 -0500 Subject: [PATCH 259/326] Remove FF (#6302) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 025871440f..cfc85f8164 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -168,7 +168,6 @@ public static class FeatureFlagKeys public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; - public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; From d384c0cfe60ec02226479d3cde200e4d785a7e50 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Fri, 19 Sep 2025 16:17:32 -0400 Subject: [PATCH 260/326] [PM-7730] Deprecate type-specific cipher properties in favor of opaque Data string (#6354) * Marked structured fields as obsolete and add Data field to the request model * Fixed lint issues * Deprecated properties * Changed to 1mb --- .../Models/Request/CipherRequestModel.cs | 74 +++++++++++++------ .../Models/Response/CipherResponseModel.cs | 26 +++++-- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 467be6e356..b0589a62f9 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -7,8 +7,6 @@ using Bit.Core.Utilities; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; -using NS = Newtonsoft.Json; -using NSL = Newtonsoft.Json.Linq; namespace Bit.Api.Vault.Models.Request; @@ -40,11 +38,26 @@ public class CipherRequestModel // TODO: Rename to Attachments whenever the above is finally removed. public Dictionary Attachments2 { get; set; } + [Obsolete("Use Data instead.")] public CipherLoginModel Login { get; set; } + + [Obsolete("Use Data instead.")] public CipherCardModel Card { get; set; } + + [Obsolete("Use Data instead.")] public CipherIdentityModel Identity { get; set; } + + [Obsolete("Use Data instead.")] public CipherSecureNoteModel SecureNote { get; set; } + + [Obsolete("Use Data instead.")] public CipherSSHKeyModel SSHKey { get; set; } + + /// + /// JSON string containing cipher-specific data + /// + [StringLength(500000)] + public string Data { get; set; } public DateTime? LastKnownRevisionDate { get; set; } = null; public DateTime? ArchivedDate { get; set; } @@ -73,29 +86,42 @@ public class CipherRequestModel public Cipher ToCipher(Cipher existingCipher) { - switch (existingCipher.Type) + // If Data field is provided, use it directly + if (!string.IsNullOrWhiteSpace(Data)) { - case CipherType.Login: - var loginObj = NSL.JObject.FromObject(ToCipherLoginData(), - new NS.JsonSerializer { NullValueHandling = NS.NullValueHandling.Ignore }); - // TODO: Switch to JsonNode in .NET 6 https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-use-dom-utf8jsonreader-utf8jsonwriter?pivots=dotnet-6-0 - loginObj[nameof(CipherLoginData.Uri)]?.Parent?.Remove(); - existingCipher.Data = loginObj.ToString(NS.Formatting.None); - break; - case CipherType.Card: - existingCipher.Data = JsonSerializer.Serialize(ToCipherCardData(), JsonHelpers.IgnoreWritingNull); - break; - case CipherType.Identity: - existingCipher.Data = JsonSerializer.Serialize(ToCipherIdentityData(), JsonHelpers.IgnoreWritingNull); - break; - case CipherType.SecureNote: - existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull); - break; - case CipherType.SSHKey: - existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull); - break; - default: - throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); + existingCipher.Data = Data; + } + else + { + // Fallback to structured fields + switch (existingCipher.Type) + { + case CipherType.Login: + var loginData = ToCipherLoginData(); + var loginJson = JsonSerializer.Serialize(loginData, JsonHelpers.IgnoreWritingNull); + var loginObj = JsonDocument.Parse(loginJson); + var loginDict = JsonSerializer.Deserialize>(loginJson); + loginDict?.Remove(nameof(CipherLoginData.Uri)); + + existingCipher.Data = JsonSerializer.Serialize(loginDict, JsonHelpers.IgnoreWritingNull); + break; + case CipherType.Card: + existingCipher.Data = JsonSerializer.Serialize(ToCipherCardData(), JsonHelpers.IgnoreWritingNull); + break; + case CipherType.Identity: + existingCipher.Data = + JsonSerializer.Serialize(ToCipherIdentityData(), JsonHelpers.IgnoreWritingNull); + break; + case CipherType.SecureNote: + existingCipher.Data = + JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull); + break; + case CipherType.SSHKey: + existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull); + break; + default: + throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); + } } existingCipher.Reprompt = Reprompt; diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index 3e4e8da512..dfacc1a551 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -24,6 +24,7 @@ public class CipherMiniResponseModel : ResponseModel Id = cipher.Id; Type = cipher.Type; + Data = cipher.Data; CipherData cipherData; switch (cipher.Type) @@ -31,30 +32,25 @@ public class CipherMiniResponseModel : ResponseModel case CipherType.Login: var loginData = JsonSerializer.Deserialize(cipher.Data); cipherData = loginData; - Data = loginData; Login = new CipherLoginModel(loginData); break; case CipherType.SecureNote: var secureNoteData = JsonSerializer.Deserialize(cipher.Data); - Data = secureNoteData; cipherData = secureNoteData; SecureNote = new CipherSecureNoteModel(secureNoteData); break; case CipherType.Card: var cardData = JsonSerializer.Deserialize(cipher.Data); - Data = cardData; cipherData = cardData; Card = new CipherCardModel(cardData); break; case CipherType.Identity: var identityData = JsonSerializer.Deserialize(cipher.Data); - Data = identityData; cipherData = identityData; Identity = new CipherIdentityModel(identityData); break; case CipherType.SSHKey: var sshKeyData = JsonSerializer.Deserialize(cipher.Data); - Data = sshKeyData; cipherData = sshKeyData; SSHKey = new CipherSSHKeyModel(sshKeyData); break; @@ -80,15 +76,33 @@ public class CipherMiniResponseModel : ResponseModel public Guid Id { get; set; } public Guid? OrganizationId { get; set; } public CipherType Type { get; set; } - public dynamic Data { get; set; } + public string Data { get; set; } + + [Obsolete("Use Data instead.")] public string Name { get; set; } + + [Obsolete("Use Data instead.")] public string Notes { get; set; } + + [Obsolete("Use Data instead.")] public CipherLoginModel Login { get; set; } + + [Obsolete("Use Data instead.")] public CipherCardModel Card { get; set; } + + [Obsolete("Use Data instead.")] public CipherIdentityModel Identity { get; set; } + + [Obsolete("Use Data instead.")] public CipherSecureNoteModel SecureNote { get; set; } + + [Obsolete("Use Data instead.")] public CipherSSHKeyModel SSHKey { get; set; } + + [Obsolete("Use Data instead.")] public IEnumerable Fields { get; set; } + + [Obsolete("Use Data instead.")] public IEnumerable PasswordHistory { get; set; } public IEnumerable Attachments { get; set; } public bool OrganizationUseTotp { get; set; } From dc2828291b0de886b36d2aebcd18d11ae8f32bab Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 22 Sep 2025 15:02:24 +0000 Subject: [PATCH 261/326] Bumped version to 2025.9.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9038c8d95d..71303d3529 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.9.1 + 2025.9.2 Bit.$(MSBuildProjectName) enable From fe7e96eb6aeaa42ac2fe7af9c3bcf74258c52abf Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Mon, 22 Sep 2025 10:36:19 -0500 Subject: [PATCH 262/326] PM-25870 Activity tab feature flag (#6360) --- src/Core/Constants.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index cfc85f8164..f596cdefee 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -233,6 +233,9 @@ public static class FeatureFlagKeys /* Innovation Team */ public const string ArchiveVaultItems = "pm-19148-innovation-archive"; + /* DIRT Team */ + public const string PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab"; + public static List GetAllKeys() { return typeof(FeatureFlagKeys).GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) From 0b6b93048b4b57d51183e2aa4dc5a1e0e9a51f8a Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Mon, 22 Sep 2025 11:05:16 -0500 Subject: [PATCH 263/326] [PM-25373] Add feature flag (#6358) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f596cdefee..96ee509db1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -184,6 +184,7 @@ public static class FeatureFlagKeys 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"; + public const string WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2"; /* Mobile Team */ public const string NativeCarouselFlow = "native-carousel-flow"; From 8c238ce08db8e5b227ebdc9a980b99445b7ed140 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Mon, 22 Sep 2025 13:46:35 -0400 Subject: [PATCH 264/326] fix: adjust permissions of repo management workflow (#6130) - Specify permissions needed for the repo_management job - Add required permissions (actions: read, contents: write, id-token: write, pull-requests: write) to the move_edd_db_scripts job --- .github/workflows/repository-management.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index ad80d5864c..67e1d8a926 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -22,7 +22,9 @@ on: required: false type: string -permissions: {} +permissions: + pull-requests: write + contents: write jobs: setup: @@ -231,5 +233,10 @@ jobs: move_edd_db_scripts: name: Move EDD database scripts needs: cut_branch + permissions: + actions: read + contents: write + id-token: write + pull-requests: write uses: ./.github/workflows/_move_edd_db_scripts.yml secrets: inherit From ed5e4271df54e54dc271905cc10efefb967eaaa7 Mon Sep 17 00:00:00 2001 From: Stephon Brown Date: Mon, 22 Sep 2025 13:51:36 -0400 Subject: [PATCH 265/326] [PM-25123] Remove VerifyBankAsync Code (#6355) * refactor: remove VerifyBankAsync from interface and implementation * refactor: remove controller endpoint --- .../Controllers/OrganizationsController.cs | 12 ------ .../Services/IOrganizationService.cs | 1 - .../Implementations/OrganizationService.cs | 43 ------------------- 3 files changed, 56 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 977b20bdfb..5494c5a90e 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -211,18 +211,6 @@ public class OrganizationsController( return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result }; } - [HttpPost("{id:guid}/verify-bank")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostVerifyBank(Guid id, [FromBody] OrganizationVerifyBankRequestModel model) - { - if (!await currentContext.EditSubscription(id)) - { - throw new NotFoundException(); - } - - await organizationService.VerifyBankAsync(id, model.Amount1.Value, model.Amount2.Value); - } - [HttpPost("{id}/cancel")] public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request) { diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 94df74afdf..f509ac8358 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -18,7 +18,6 @@ public interface IOrganizationService Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats); Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); - Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateAsync(Organization organization, bool updateBilling = false); Task UpdateCollectionManagementSettingsAsync(Guid organizationId, OrganizationCollectionManagementSettings settings); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 57eb4f51de..1b52ad8cff 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -325,49 +325,6 @@ public class OrganizationService : IOrganizationService return paymentIntentClientSecret; } - public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) - { - throw new GatewayException("Not a gateway customer."); - } - - var bankService = new BankAccountService(); - var customer = await _stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId, - new CustomerGetOptions { Expand = new List { "sources" } }); - if (customer == null) - { - throw new GatewayException("Cannot find customer."); - } - - var bankAccount = customer.Sources - .FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount; - if (bankAccount == null) - { - throw new GatewayException("Cannot find an unverified bank account."); - } - - try - { - var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id, - new BankAccountVerifyOptions { Amounts = new List { amount1, amount2 } }); - if (result.Status != "verified") - { - throw new GatewayException("Unable to verify account."); - } - } - catch (StripeException e) - { - throw new GatewayException(e.Message); - } - } - public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); From c6f5d5e36e830642c6e028732aa66cb6a9583ad2 Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 22 Sep 2025 15:39:15 -0400 Subject: [PATCH 266/326] [PM-25986] Add server side enum type for AutotypeDefaultSetting policy (#6356) * PM-25986 Add server side enum type for AutotypeDefaultSetting policy * Update PolicyType.cs remove space --- src/Core/AdminConsole/Enums/PolicyType.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index f038faaf0b..452fbcce01 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -19,6 +19,7 @@ public enum PolicyType : byte RemoveUnlockWithPin = 14, RestrictedItemTypesPolicy = 15, UriMatchDefaults = 16, + AutotypeDefaultSetting = 17, } public static class PolicyTypeExtensions @@ -48,6 +49,7 @@ public static class PolicyTypeExtensions PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN", PolicyType.RestrictedItemTypesPolicy => "Restricted item types", PolicyType.UriMatchDefaults => "URI match defaults", + PolicyType.AutotypeDefaultSetting => "Autotype default setting", }; } } From 3b54fea309fc9612389a77785fd7f5470366370a Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Tue, 23 Sep 2025 06:38:22 -0400 Subject: [PATCH 267/326] [PM-22696] send enumeration protection (#6352) * feat: add static enumeration helper class * test: add enumeration helper class unit tests * feat: implement NeverAuthenticateValidator * test: unit and integration tests SendNeverAuthenticateValidator * test: use static class for common integration test setup for Send Access unit and integration tests * test: update tests to use static helper --- src/Core/Settings/GlobalSettings.cs | 4 + .../Utilities/EnumerationProtectionHelpers.cs | 36 +++ .../SendAccess/SendAccessConstants.cs | 22 +- .../SendAccess/SendAccessGrantValidator.cs | 37 +-- .../SendNeverAuthenticateRequestValidator.cs | 87 ++++++ .../Utilities/ServiceCollectionExtensions.cs | 1 + .../EnumerationProtectionHelpersTests.cs | 230 ++++++++++++++ ...endAccessGrantValidatorIntegrationTests.cs | 53 +--- .../SendAccess/SendAccessTestUtilities.cs | 45 +++ ...EmailOtpReqestValidatorIntegrationTests.cs | 54 +--- ...ndNeverAuthenticateRequestValidatorTest.cs | 168 +++++++++++ ...asswordRequestValidatorIntegrationTests.cs | 45 +-- .../ResourceOwnerPasswordValidatorTests.cs | 2 +- .../SendAccessGrantValidatorTests.cs | 63 +--- .../SendAccess/SendAccessTestUtilities.cs | 50 ++++ .../SendAccess/SendConstantsSnapshotTests.cs | 6 +- .../SendEmailOtpRequestValidatorTests.cs | 49 +-- .../SendNeverAuthenticateValidatorTests.cs | 280 ++++++++++++++++++ .../SendPasswordRequestValidatorTests.cs | 47 +-- 19 files changed, 989 insertions(+), 290 deletions(-) create mode 100644 src/Core/Utilities/EnumerationProtectionHelpers.cs create mode 100644 src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs create mode 100644 test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs rename test/Identity.IntegrationTest/RequestValidation/{ => SendAccess}/SendAccessGrantValidatorIntegrationTests.cs (81%) create mode 100644 test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs rename test/Identity.IntegrationTest/RequestValidation/{ => SendAccess}/SendEmailOtpReqestValidatorIntegrationTests.cs (79%) create mode 100644 test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs rename test/Identity.IntegrationTest/RequestValidation/{ => SendAccess}/SendPasswordRequestValidatorIntegrationTests.cs (80%) rename test/Identity.IntegrationTest/RequestValidation/{ => VaultAccess}/ResourceOwnerPasswordValidatorTests.cs (99%) create mode 100644 test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs create mode 100644 test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index c6c96cffb9..107fd29236 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -92,6 +92,10 @@ public class GlobalSettings : IGlobalSettings public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5; public virtual bool EnableEmailVerification { get; set; } public virtual string KdfDefaultHashKey { get; set; } + /// + /// This Hash Key is used to prevent enumeration attacks against the Send Access feature. + /// + public virtual string SendDefaultHashKey { get; set; } public virtual string PricingUri { get; set; } public string BuildExternalUri(string explicitValue, string name) diff --git a/src/Core/Utilities/EnumerationProtectionHelpers.cs b/src/Core/Utilities/EnumerationProtectionHelpers.cs new file mode 100644 index 0000000000..b27c36e03a --- /dev/null +++ b/src/Core/Utilities/EnumerationProtectionHelpers.cs @@ -0,0 +1,36 @@ +using System.Text; + +namespace Bit.Core.Utilities; + +public static class EnumerationProtectionHelpers +{ + /// + /// Use this method to get a consistent int result based on the inputString that is in the range. + /// The same inputString will always return the same index result based on range input. + /// + /// Key used to derive the HMAC hash. Use a different key for each usage for optimal security + /// The string to derive an index result + /// The range of possible index values + /// An int between 0 and range - 1 + public static int GetIndexForInputHash(byte[] hmacKey, string inputString, int range) + { + if (hmacKey == null || range <= 0 || hmacKey.Length == 0) + { + return 0; + } + else + { + // Compute the HMAC hash of the salt + var hmacMessage = Encoding.UTF8.GetBytes(inputString.Trim().ToLowerInvariant()); + using var hmac = new System.Security.Cryptography.HMACSHA256(hmacKey); + var hmacHash = hmac.ComputeHash(hmacMessage); + // Convert the hash to a number + var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant(); + var hashFirst8Bytes = hashHex[..16]; + var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber); + // Find the default KDF value for this hash number + var hashIndex = (int)(Math.Abs(hashNumber) % range); + return hashIndex; + } + } +} diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs index 17ec387411..1f5bfba244 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessConstants.cs @@ -34,18 +34,18 @@ public static class SendAccessConstants public const string Otp = "otp"; } - public static class GrantValidatorResults + public static class SendIdGuidValidatorResults { /// - /// The sendId is valid and the request is well formed. Not returned in any response. + /// The in the request is a valid GUID and the request is well formed. Not returned in any response. /// public const string ValidSendGuid = "valid_send_guid"; /// - /// The sendId is missing from the request. + /// The is missing from the request. /// public const string SendIdRequired = "send_id_required"; /// - /// The sendId is invalid, does not match a known send. + /// The is invalid, does not match a known send. /// public const string InvalidSendId = "send_id_invalid"; } @@ -53,11 +53,11 @@ public static class SendAccessConstants public static class PasswordValidatorResults { /// - /// The passwordHashB64 does not match the send's password hash. + /// The does not match the send's password hash. /// public const string RequestPasswordDoesNotMatch = "password_hash_b64_invalid"; /// - /// The passwordHashB64 is missing from the request. + /// The is missing from the request. /// public const string RequestPasswordIsRequired = "password_hash_b64_required"; } @@ -105,4 +105,14 @@ public static class SendAccessConstants { public const string Subject = "Your Bitwarden Send verification code is {0}"; } + + /// + /// We use these static strings to help guide the enumeration protection logic. + /// + public static class EnumerationProtection + { + public const string Guid = "guid"; + public const string Password = "password"; + public const string Email = "email"; + } } diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs index d9ae946d16..101c6952f3 100644 --- a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendAccessGrantValidator.cs @@ -13,21 +13,19 @@ namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; public class SendAccessGrantValidator( ISendAuthenticationQuery _sendAuthenticationQuery, + ISendAuthenticationMethodValidator _sendNeverAuthenticateValidator, ISendAuthenticationMethodValidator _sendPasswordRequestValidator, ISendAuthenticationMethodValidator _sendEmailOtpRequestValidator, - IFeatureService _featureService) -: IExtensionGrantValidator + IFeatureService _featureService) : IExtensionGrantValidator { string IExtensionGrantValidator.GrantType => CustomGrantTypes.SendAccess; - private static readonly Dictionary - _sendGrantValidatorErrorDescriptions = new() + private static readonly Dictionary _sendGrantValidatorErrorDescriptions = new() { - { SendAccessConstants.GrantValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." }, - { SendAccessConstants.GrantValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." } + { SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired, $"{SendAccessConstants.TokenRequest.SendId} is required." }, + { SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, $"{SendAccessConstants.TokenRequest.SendId} is invalid." } }; - public async Task ValidateAsync(ExtensionGrantValidationContext context) { // Check the feature flag @@ -38,7 +36,7 @@ public class SendAccessGrantValidator( } var (sendIdGuid, result) = GetRequestSendId(context); - if (result != SendAccessConstants.GrantValidatorResults.ValidSendGuid) + if (result != SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid) { context.Result = BuildErrorResult(result); return; @@ -49,15 +47,10 @@ public class SendAccessGrantValidator( switch (method) { - case NeverAuthenticate: + case NeverAuthenticate never: // null send scenario. - // TODO PM-22675: Add send enumeration protection here (primarily benefits self hosted instances). - // We should only map to password or email + OTP protected. - // If user submits password guess for a falsely protected send, then we will return invalid password. - // If user submits email + OTP guess for a falsely protected send, then we will return email sent, do not actually send an email. - context.Result = BuildErrorResult(SendAccessConstants.GrantValidatorResults.InvalidSendId); + context.Result = await _sendNeverAuthenticateValidator.ValidateRequestAsync(context, never, sendIdGuid); return; - case NotAuthenticated: // automatically issue access token context.Result = BuildBaseSuccessResult(sendIdGuid); @@ -90,7 +83,7 @@ public class SendAccessGrantValidator( // if the sendId is null then the request is the wrong shape and the request is invalid if (sendId == null) { - return (Guid.Empty, SendAccessConstants.GrantValidatorResults.SendIdRequired); + return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired); } // the send_id is not null so the request is the correct shape, so we will attempt to parse it try @@ -100,20 +93,20 @@ public class SendAccessGrantValidator( // Guid.Empty indicates an invalid send_id return invalid grant if (sendGuid == Guid.Empty) { - return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId); + return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); } - return (sendGuid, SendAccessConstants.GrantValidatorResults.ValidSendGuid); + return (sendGuid, SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid); } catch { - return (Guid.Empty, SendAccessConstants.GrantValidatorResults.InvalidSendId); + return (Guid.Empty, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); } } /// /// Builds an error result for the specified error type. /// - /// This error is a constant string from + /// This error is a constant string from /// The error result. private static GrantValidationResult BuildErrorResult(string error) { @@ -125,12 +118,12 @@ public class SendAccessGrantValidator( return error switch { // Request is the wrong shape - SendAccessConstants.GrantValidatorResults.SendIdRequired => new GrantValidationResult( + SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired => new GrantValidationResult( TokenRequestErrors.InvalidRequest, errorDescription: _sendGrantValidatorErrorDescriptions[error], customResponse), // Request is correct shape but data is bad - SendAccessConstants.GrantValidatorResults.InvalidSendId => new GrantValidationResult( + SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId => new GrantValidationResult( TokenRequestErrors.InvalidGrant, errorDescription: _sendGrantValidatorErrorDescriptions[error], customResponse), diff --git a/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs new file mode 100644 index 0000000000..36e033360f --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/SendAccess/SendNeverAuthenticateRequestValidator.cs @@ -0,0 +1,87 @@ +using System.Text; +using Bit.Core.Settings; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Utilities; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators.SendAccess; + +/// +/// This class is used to protect our system from enumeration attacks. This Validator will always return an error result. +/// We hash the SendId Guid passed into the request to select the an error from the list of possible errors. This ensures +/// that the same error is always returned for the same SendId. +/// +/// We need access to a hash key to generate the error index. +public class SendNeverAuthenticateRequestValidator(GlobalSettings globalSettings) : ISendAuthenticationMethodValidator +{ + private readonly string[] _errorOptions = + [ + SendAccessConstants.EnumerationProtection.Guid, + SendAccessConstants.EnumerationProtection.Password, + SendAccessConstants.EnumerationProtection.Email + ]; + + public Task ValidateRequestAsync( + ExtensionGrantValidationContext context, + NeverAuthenticate authMethod, + Guid sendId) + { + var neverAuthenticateError = GetErrorIndex(sendId, _errorOptions.Length); + var request = context.Request.Raw; + var errorType = neverAuthenticateError; + + switch (neverAuthenticateError) + { + case SendAccessConstants.EnumerationProtection.Guid: + errorType = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId; + break; + case SendAccessConstants.EnumerationProtection.Email: + var hasEmail = request.Get(SendAccessConstants.TokenRequest.Email) is not null; + errorType = hasEmail ? SendAccessConstants.EmailOtpValidatorResults.EmailInvalid + : SendAccessConstants.EmailOtpValidatorResults.EmailRequired; + break; + case SendAccessConstants.EnumerationProtection.Password: + var hasPassword = request.Get(SendAccessConstants.TokenRequest.ClientB64HashedPassword) is not null; + errorType = hasPassword ? SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch + : SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired; + break; + } + + return Task.FromResult(BuildErrorResult(errorType)); + } + + private static GrantValidationResult BuildErrorResult(string errorType) + { + // Create error response with custom response data + var customResponse = new Dictionary + { + { SendAccessConstants.SendAccessError, errorType } + }; + + var requestError = errorType switch + { + SendAccessConstants.EnumerationProtection.Guid => TokenRequestErrors.InvalidGrant, + SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired => TokenRequestErrors.InvalidGrant, + SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch => TokenRequestErrors.InvalidRequest, + SendAccessConstants.EmailOtpValidatorResults.EmailInvalid => TokenRequestErrors.InvalidGrant, + SendAccessConstants.EmailOtpValidatorResults.EmailRequired => TokenRequestErrors.InvalidRequest, + _ => TokenRequestErrors.InvalidGrant + }; + + return new GrantValidationResult(requestError, errorType, customResponse); + } + + private string GetErrorIndex(Guid sendId, int range) + { + var salt = sendId.ToString(); + byte[] hmacKey = []; + if (CoreHelpers.SettingHasValue(globalSettings.SendDefaultHashKey)) + { + hmacKey = Encoding.UTF8.GetBytes(globalSettings.SendDefaultHashKey); + } + + var index = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + return _errorOptions[index]; + } +} diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 9d062e5c06..e9056d030e 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient, SendPasswordRequestValidator>(); services.AddTransient, SendEmailOtpRequestValidator>(); + services.AddTransient, SendNeverAuthenticateRequestValidator>(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs b/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs new file mode 100644 index 0000000000..68ac8af5d0 --- /dev/null +++ b/test/Core.Test/Utilities/EnumerationProtectionHelpersTests.cs @@ -0,0 +1,230 @@ +using System.Security.Cryptography; +using System.Text; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Core.Test.Utilities; + +public class EnumerationProtectionHelpersTests +{ + #region GetIndexForInputHash Tests + + [Fact] + public void GetIndexForInputHash_NullHmacKey_ReturnsZero() + { + // Arrange + byte[] hmacKey = null; + var salt = "test@example.com"; + var range = 10; + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetIndexForInputHash_ZeroRange_ReturnsZero() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "test@example.com"; + var range = 0; + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetIndexForInputHash_NegativeRange_ReturnsZero() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "test@example.com"; + var range = -5; + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetIndexForInputHash_ValidInputs_ReturnsConsistentResult() + { + // Arrange + var hmacKey = Encoding.UTF8.GetBytes("test-key-12345678901234567890123456789012"); + var salt = "test@example.com"; + var range = 10; + + // Act + var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(result1, result2); + Assert.InRange(result1, 0, range - 1); + } + + [Fact] + public void GetIndexForInputHash_SameInputSameKey_AlwaysReturnsSameResult() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "consistent@example.com"; + var range = 100; + + // Act - Call multiple times + var results = new int[10]; + for (var i = 0; i < 10; i++) + { + results[i] = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + } + + // Assert - All results should be identical + Assert.All(results, result => Assert.Equal(results[0], result)); + Assert.All(results, result => Assert.InRange(result, 0, range - 1)); + } + + [Fact] + public void GetIndexForInputHash_DifferentInputsSameKey_ReturnsDifferentResults() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt1 = "user1@example.com"; + var salt2 = "user2@example.com"; + var range = 100; + + // Act + var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt1, range); + var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt2, range); + + // Assert + Assert.NotEqual(result1, result2); + Assert.InRange(result1, 0, range - 1); + Assert.InRange(result2, 0, range - 1); + } + + [Fact] + public void GetIndexForInputHash_DifferentKeysSameInput_ReturnsDifferentResults() + { + // Arrange + var hmacKey1 = RandomNumberGenerator.GetBytes(32); + var hmacKey2 = RandomNumberGenerator.GetBytes(32); + var salt = "test@example.com"; + var range = 100; + + // Act + var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey1, salt, range); + var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey2, salt, range); + + // Assert + Assert.NotEqual(result1, result2); + Assert.InRange(result1, 0, range - 1); + Assert.InRange(result2, 0, range - 1); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + [InlineData(10)] + [InlineData(100)] + [InlineData(1000)] + public void GetIndexForInputHash_VariousRanges_ReturnsValidIndex(int range) + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "test@example.com"; + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.InRange(result, 0, range - 1); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GetIndexForInputHash_EmptyString_HandlesGracefully(string salt) + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, 10); + + // Assert + Assert.InRange(result, 0, 9); + } + + [Fact] + public void GetIndexForInputHash_NullInput_ThrowsException() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + string salt = null; + var range = 10; + + // Act & Assert + Assert.Throws(() => + EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range)); + } + + [Fact] + public void GetIndexForInputHash_SpecialCharacters_HandlesCorrectly() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "test+user@example.com!@#$%^&*()"; + var range = 50; + + // Act + var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(result1, result2); + Assert.InRange(result1, 0, range - 1); + } + + [Fact] + public void GetIndexForInputHash_UnicodeCharacters_HandlesCorrectly() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = "tëst@éxämplé.cöm"; + var range = 25; + + // Act + var result1 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + var result2 = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.Equal(result1, result2); + Assert.InRange(result1, 0, range - 1); + } + + [Fact] + public void GetIndexForInputHash_LongInput_HandlesCorrectly() + { + // Arrange + var hmacKey = RandomNumberGenerator.GetBytes(32); + var salt = new string('a', 1000) + "@example.com"; + var range = 30; + + // Act + var result = EnumerationProtectionHelpers.GetIndexForInputHash(hmacKey, salt, range); + + // Assert + Assert.InRange(result, 0, range - 1); + } + + #endregion +} diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs similarity index 81% rename from test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs index ca6417d49c..a6dd4b6b38 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendAccessGrantValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessGrantValidatorIntegrationTests.cs @@ -1,10 +1,8 @@ using Bit.Core; -using Bit.Core.Auth.IdentityServer; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; -using Bit.Core.Utilities; using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; @@ -13,16 +11,14 @@ using Duende.IdentityServer.Validation; using NSubstitute; using Xunit; -namespace Bit.Identity.IntegrationTest.RequestValidation; +namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; // in order to test the default case for the authentication method, we need to create a custom one so we can ensure the // method throws as expected. internal record AnUnknownAuthenticationMethod : SendAuthenticationMethod { } -public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory factory) : IClassFixture +public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture { - private readonly IdentityApplicationFactory _factory = factory; - [Fact] public async Task SendAccessGrant_FeatureFlagDisabled_ReturnsUnsupportedGrantType() { @@ -39,7 +35,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); @@ -70,7 +66,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); @@ -125,7 +121,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); @@ -154,7 +150,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var response = await client.PostAsync("/connect/token", requestBody); @@ -183,7 +179,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId); // Act var error = await client.PostAsync("/connect/token", requestBody); @@ -225,7 +221,7 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory }); }).CreateClient(); - var requestBody = CreateTokenRequestBody(sendId, "password123"); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(sendId, "password123"); // Act var response = await client.PostAsync("/connect/token", requestBody); @@ -236,37 +232,4 @@ public class SendAccessGrantValidatorIntegrationTests(IdentityApplicationFactory Assert.Contains("access_token", content); Assert.Contains("Bearer", content); } - - private static FormUrlEncodedContent CreateTokenRequestBody( - Guid sendId, - string password = null, - string sendEmail = null, - string emailOtp = null) - { - var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); - var parameters = new List> - { - new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), - new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ), - new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), - new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()), - new(SendAccessConstants.TokenRequest.SendId, sendIdBase64) - }; - - if (!string.IsNullOrEmpty(password)) - { - parameters.Add(new(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password)); - } - - if (!string.IsNullOrEmpty(emailOtp) && !string.IsNullOrEmpty(sendEmail)) - { - parameters.AddRange( - [ - new KeyValuePair("email", sendEmail), - new KeyValuePair("email_otp", emailOtp) - ]); - } - - return new FormUrlEncodedContent(parameters); - } } diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs new file mode 100644 index 0000000000..7842bf6367 --- /dev/null +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendAccessTestUtilities.cs @@ -0,0 +1,45 @@ +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Duende.IdentityModel; + +namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; + +public static class SendAccessTestUtilities +{ + public static FormUrlEncodedContent CreateTokenRequestBody( + Guid sendId, + string email = null, + string emailOtp = null, + string password = null) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + var parameters = new List> + { + new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), + new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send), + new(SendAccessConstants.TokenRequest.SendId, sendIdBase64), + new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), + new("device_type", "10") + }; + + if (!string.IsNullOrEmpty(email)) + { + parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.Email, email)); + } + + if (!string.IsNullOrEmpty(emailOtp)) + { + parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.Otp, emailOtp)); + } + + if (!string.IsNullOrEmpty(password)) + { + parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.ClientB64HashedPassword, password)); + } + + return new FormUrlEncodedContent(parameters); + } +} diff --git a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs similarity index 79% rename from test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs index 9a097cc061..3c4657653b 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendEmailOtpReqestValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendEmailOtpReqestValidatorIntegrationTests.cs @@ -1,28 +1,16 @@ using Bit.Core.Auth.Identity.TokenProviders; -using Bit.Core.Auth.IdentityServer; -using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; -using Bit.Core.Utilities; -using Bit.Identity.IdentityServer.Enums; -using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; using Duende.IdentityModel; using NSubstitute; using Xunit; -namespace Bit.Identity.IntegrationTest.RequestValidation; +namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; -public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture +public class SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture { - private readonly IdentityApplicationFactory _factory; - - public SendEmailOtpRequestValidatorIntegrationTests(IdentityApplicationFactory factory) - { - _factory = factory; - } - [Fact] public async Task SendAccess_EmailOtpProtectedSend_MissingEmail_ReturnsInvalidRequest() { @@ -43,7 +31,7 @@ public class SendEmailOtpRequestValidatorIntegrationTests : IClassFixture> - { - new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), - new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send ), - new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), - new("deviceType", ((int)DeviceType.FirefoxBrowser).ToString()), - new(SendAccessConstants.TokenRequest.SendId, sendIdBase64) - }; - - if (!string.IsNullOrEmpty(sendEmail)) - { - parameters.Add(new KeyValuePair( - SendAccessConstants.TokenRequest.Email, sendEmail)); - } - - if (!string.IsNullOrEmpty(emailOtp)) - { - parameters.Add(new KeyValuePair( - SendAccessConstants.TokenRequest.Otp, emailOtp)); - } - - return new FormUrlEncodedContent(parameters); - } } diff --git a/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs new file mode 100644 index 0000000000..a81b01a293 --- /dev/null +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendNeverAuthenticateRequestValidatorTest.cs @@ -0,0 +1,168 @@ +using Bit.Core.Services; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.IntegrationTestCommon.Factories; +using Duende.IdentityModel; +using NSubstitute; +using Xunit; + +namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; + +public class SendNeverAuthenticateRequestValidatorIntegrationTests( + IdentityApplicationFactory _factory) : IClassFixture +{ + /// + /// To support the static hashing function theses GUIDs and Key must be hardcoded + /// + private static readonly string _testHashKey = "test-key-123456789012345678901234567890"; + // These Guids are static and ensure the correct index for each error type + private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f"); + private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41"); + private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b"); + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_NoParameters_ReturnsInvalidSendId() + { + // Arrange + var client = ConfigureTestHttpClient(_invalidSendGuid); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_invalidSendGuid); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + + Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); + + var expectedError = SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId; + Assert.Contains(expectedError, content); + } + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_ReturnsEmailRequired() + { + // Arrange + var client = ConfigureTestHttpClient(_emailSendGuid); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + + // should be invalid grant + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + + // Try to compel the invalid email error + var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailRequired; + Assert.Contains(expectedError, content); + } + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_WithEmail_ReturnsEmailInvalid() + { + // Arrange + var email = "test@example.com"; + var client = ConfigureTestHttpClient(_emailSendGuid); + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid, email: email); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + + // should be invalid grant + Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); + + // Try to compel the invalid email error + var expectedError = SendAccessConstants.EmailOtpValidatorResults.EmailInvalid; + Assert.Contains(expectedError, content); + } + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_ReturnsPasswordRequired() + { + // Arrange + var client = ConfigureTestHttpClient(_passwordSendGuid); + + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + + Assert.Contains(OidcConstants.TokenErrors.InvalidGrant, content); + + var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired; + Assert.Contains(expectedError, content); + } + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_WithPassword_ReturnsPasswordInvalid() + { + // Arrange + var password = "test-password-hash"; + + var client = ConfigureTestHttpClient(_passwordSendGuid); + + var requestBody = SendAccessTestUtilities.CreateTokenRequestBody(_passwordSendGuid, password: password); + + // Act + var response = await client.PostAsync("/connect/token", requestBody); + + // Assert + var content = await response.Content.ReadAsStringAsync(); + + Assert.Contains(OidcConstants.TokenErrors.InvalidRequest, content); + + var expectedError = SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch; + Assert.Contains(expectedError, content); + } + + [Fact] + public async Task SendAccess_NeverAuthenticateSend_ConsistentResponse_SameSendId() + { + // Arrange + var client = ConfigureTestHttpClient(_emailSendGuid); + + var requestBody1 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid); + var requestBody2 = SendAccessTestUtilities.CreateTokenRequestBody(_emailSendGuid); + + // Act + var response1 = await client.PostAsync("/connect/token", requestBody1); + var response2 = await client.PostAsync("/connect/token", requestBody2); + + // Assert + var content1 = await response1.Content.ReadAsStringAsync(); + var content2 = await response2.Content.ReadAsStringAsync(); + + Assert.Equal(content1, content2); + } + + private HttpClient ConfigureTestHttpClient(Guid sendId) + { + _factory.UpdateConfiguration( + "globalSettings:sendDefaultHashKey", _testHashKey); + return _factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + var featureService = Substitute.For(); + featureService.IsEnabled(Arg.Any()).Returns(true); + services.AddSingleton(featureService); + + var sendAuthQuery = Substitute.For(); + sendAuthQuery.GetAuthenticationMethod(sendId) + .Returns(new NeverAuthenticate()); + services.AddSingleton(sendAuthQuery); + }); + }).CreateClient(); + } +} diff --git a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs similarity index 80% rename from test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs rename to test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs index 856ffe1f6e..5b03a298ed 100644 --- a/test/Identity.IntegrationTest/RequestValidation/SendPasswordRequestValidatorIntegrationTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/SendAccess/SendPasswordRequestValidatorIntegrationTests.cs @@ -1,28 +1,17 @@ -using Bit.Core.Auth.IdentityServer; -using Bit.Core.Enums; -using Bit.Core.KeyManagement.Sends; +using Bit.Core.KeyManagement.Sends; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; -using Bit.Core.Utilities; -using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.IntegrationTestCommon.Factories; using Duende.IdentityModel; using NSubstitute; using Xunit; -namespace Bit.Identity.IntegrationTest.RequestValidation; +namespace Bit.Identity.IntegrationTest.RequestValidation.SendAccess; -public class SendPasswordRequestValidatorIntegrationTests : IClassFixture +public class SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory _factory) : IClassFixture { - private readonly IdentityApplicationFactory _factory; - - public SendPasswordRequestValidatorIntegrationTests(IdentityApplicationFactory factory) - { - _factory = factory; - } - [Fact] public async Task SendAccess_PasswordProtectedSend_ValidPassword_ReturnsAccessToken() { @@ -54,7 +43,7 @@ public class SendPasswordRequestValidatorIntegrationTests : IClassFixture> - { - new(OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess), - new(OidcConstants.TokenRequest.ClientId, BitwardenClient.Send), - new(SendAccessConstants.TokenRequest.SendId, sendIdBase64), - new(OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess), - new("deviceType", "10") - }; - - if (passwordHash != null) - { - parameters.Add(new KeyValuePair(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash)); - } - - return new FormUrlEncodedContent(parameters); - } } diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs similarity index 99% rename from test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs rename to test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs index 4d243906af..91123b3a60 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs @@ -12,7 +12,7 @@ using Bit.Test.Common.Helpers; using Microsoft.AspNetCore.Identity; using Xunit; -namespace Bit.Identity.IntegrationTest.RequestValidation; +namespace Bit.Identity.IntegrationTest.RequestValidation.VaultAccess; public class ResourceOwnerPasswordValidatorTests : IClassFixture { diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs index 017ad70354..59d8dee2e2 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessGrantValidatorTests.cs @@ -1,12 +1,8 @@ -using System.Collections.Specialized; -using Bit.Core; +using Bit.Core; using Bit.Core.Auth.Identity; -using Bit.Core.Auth.IdentityServer; -using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.SendFeatures.Queries.Interfaces; -using Bit.Core.Utilities; using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.Test.Common.AutoFixture; @@ -81,7 +77,7 @@ public class SendAccessGrantValidatorTests var context = new ExtensionGrantValidationContext(); tokenRequest.GrantType = CustomGrantTypes.SendAccess; - tokenRequest.Raw = CreateTokenRequestBody(Guid.Empty); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(Guid.Empty); // To preserve the CreateTokenRequestBody method for more general usage we over write the sendId tokenRequest.Raw.Set(SendAccessConstants.TokenRequest.SendId, "invalid-guid-format"); @@ -118,7 +114,9 @@ public class SendAccessGrantValidatorTests public async Task ValidateAsync_NeverAuthenticateMethod_ReturnsInvalidGrant( [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, SutProvider sutProvider, - Guid sendId) + NeverAuthenticate neverAuthenticate, + Guid sendId, + GrantValidationResult expectedResult) { // Arrange var context = SetupTokenRequest( @@ -128,14 +126,20 @@ public class SendAccessGrantValidatorTests sutProvider.GetDependency() .GetAuthenticationMethod(sendId) - .Returns(new NeverAuthenticate()); + .Returns(neverAuthenticate); + + sutProvider.GetDependency>() + .ValidateRequestAsync(context, neverAuthenticate, sendId) + .Returns(expectedResult); // Act await sutProvider.Sut.ValidateAsync(context); // Assert - Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.Result.Error); - Assert.Equal($"{SendAccessConstants.TokenRequest.SendId} is invalid.", context.Result.ErrorDescription); + Assert.Equal(expectedResult, context.Result); + await sutProvider.GetDependency>() + .Received(1) + .ValidateRequestAsync(context, neverAuthenticate, sendId); } [Theory, BitAutoData] @@ -264,7 +268,7 @@ public class SendAccessGrantValidatorTests public void GrantType_ReturnsCorrectType() { // Arrange & Act - var validator = new SendAccessGrantValidator(null!, null!, null!, null!); + var validator = new SendAccessGrantValidator(null!, null!, null!, null!, null!); // Assert Assert.Equal(CustomGrantTypes.SendAccess, ((IExtensionGrantValidator)validator).GrantType); @@ -289,44 +293,9 @@ public class SendAccessGrantValidatorTests var context = new ExtensionGrantValidationContext(); request.GrantType = CustomGrantTypes.SendAccess; - request.Raw = CreateTokenRequestBody(sendId); + request.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); context.Request = request; return context; } - - private static NameValueCollection CreateTokenRequestBody( - Guid sendId, - string passwordHash = null, - string sendEmail = null, - string otpCode = null) - { - var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); - - var rawRequestParameters = new NameValueCollection - { - { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, - { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, - { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, - { "deviceType", ((int)DeviceType.FirefoxBrowser).ToString() }, - { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } - }; - - if (passwordHash != null) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, passwordHash); - } - - if (sendEmail != null) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail); - } - - if (otpCode != null && sendEmail != null) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode); - } - - return rawRequestParameters; - } } diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs b/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs new file mode 100644 index 0000000000..b05380902a --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccess/SendAccessTestUtilities.cs @@ -0,0 +1,50 @@ +using System.Collections.Specialized; +using Bit.Core.Auth.IdentityServer; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Bit.Identity.IdentityServer.Enums; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Duende.IdentityModel; + +namespace Bit.Identity.Test.IdentityServer.SendAccess; + +public static class SendAccessTestUtilities +{ + public static NameValueCollection CreateValidatedTokenRequest( + Guid sendId, + string sendEmail = null, + string otpCode = null, + params string[] passwordHash) + { + var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); + + var rawRequestParameters = new NameValueCollection + { + { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, + { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, + { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, + { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, + { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } + }; + + if (sendEmail != null) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail); + } + + if (otpCode != null && sendEmail != null) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode); + } + + if (passwordHash != null && passwordHash.Length > 0) + { + foreach (var hash in passwordHash) + { + rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash); + } + } + + return rawRequestParameters; + } +} diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs index 95a0a6675b..96a097a53c 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendConstantsSnapshotTests.cs @@ -31,9 +31,9 @@ public class SendConstantsSnapshotTests public void GrantValidatorResults_Constants_HaveCorrectValues() { // Assert - Assert.Equal("valid_send_guid", SendAccessConstants.GrantValidatorResults.ValidSendGuid); - Assert.Equal("send_id_required", SendAccessConstants.GrantValidatorResults.SendIdRequired); - Assert.Equal("send_id_invalid", SendAccessConstants.GrantValidatorResults.InvalidSendId); + Assert.Equal("valid_send_guid", SendAccessConstants.SendIdGuidValidatorResults.ValidSendGuid); + Assert.Equal("send_id_required", SendAccessConstants.SendIdGuidValidatorResults.SendIdRequired); + Assert.Equal("send_id_invalid", SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); } [Fact] diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs index 70a1585d8b..46f61cb333 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendEmailOtpRequestValidatorTests.cs @@ -1,12 +1,7 @@ -using System.Collections.Specialized; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; -using Bit.Core.Auth.IdentityServer; -using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Tools.Models.Data; -using Bit.Core.Utilities; -using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -28,7 +23,7 @@ public class SendEmailOtpRequestValidatorTests Guid sendId) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -61,8 +56,7 @@ public class SendEmailOtpRequestValidatorTests Guid sendId) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); - var emailOTP = new EmailOtp(["user@test.dev"]); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -96,7 +90,7 @@ public class SendEmailOtpRequestValidatorTests string generatedToken) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -144,7 +138,7 @@ public class SendEmailOtpRequestValidatorTests string email) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -179,7 +173,7 @@ public class SendEmailOtpRequestValidatorTests string otp) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, otp); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, otp); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -231,7 +225,7 @@ public class SendEmailOtpRequestValidatorTests string invalidOtp) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, email, invalidOtp); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, email, invalidOtp); var context = new ExtensionGrantValidationContext { Request = tokenRequest @@ -278,33 +272,4 @@ public class SendEmailOtpRequestValidatorTests // Assert Assert.NotNull(validator); } - - private static NameValueCollection CreateValidatedTokenRequest( - Guid sendId, - string sendEmail = null, - string otpCode = null) - { - var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); - - var rawRequestParameters = new NameValueCollection - { - { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, - { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, - { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, - { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, - { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } - }; - - if (sendEmail != null) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.Email, sendEmail); - } - - if (otpCode != null && sendEmail != null) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.Otp, otpCode); - } - - return rawRequestParameters; - } } diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs new file mode 100644 index 0000000000..ae0434af83 --- /dev/null +++ b/test/Identity.Test/IdentityServer/SendAccess/SendNeverAuthenticateValidatorTests.cs @@ -0,0 +1,280 @@ +using Bit.Core.Tools.Models.Data; +using Bit.Identity.IdentityServer.RequestValidators.SendAccess; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; +using Duende.IdentityServer.Validation; +using Xunit; + +namespace Bit.Identity.Test.IdentityServer.SendAccess; + +[SutProviderCustomize] +public class SendNeverAuthenticateRequestValidatorTests +{ + /// + /// To support the static hashing function theses GUIDs and Key must be hardcoded + /// + private static readonly string _testHashKey = "test-key-123456789012345678901234567890"; + // These Guids are static and ensure the correct index for each error type + private static readonly Guid _invalidSendGuid = Guid.Parse("1b35fbf3-a14a-4d48-82b7-2adc34fdae6f"); + private static readonly Guid _emailSendGuid = Guid.Parse("bc8e2ef5-a0bd-44d2-bdb7-5902be6f5c41"); + private static readonly Guid _passwordSendGuid = Guid.Parse("da36fa27-f0e8-4701-a585-d3d8c2f67c4b"); + + private static readonly NeverAuthenticate _authMethod = new(); + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_GuidErrorSelected_ReturnsInvalidSendId( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal(SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, result.ErrorDescription); + + var customResponse = result.CustomResponse as Dictionary; + Assert.NotNull(customResponse); + Assert.Equal( + SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, customResponse[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_EmailErrorSelected_HasEmail_ReturnsEmailInvalid( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + string email) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid, sendEmail: email); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, result.ErrorDescription); + + var customResponse = result.CustomResponse as Dictionary; + Assert.NotNull(customResponse); + Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid, customResponse[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_EmailErrorSelected_NoEmail_ReturnsEmailRequired( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_emailSendGuid); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _emailSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, result.ErrorDescription); + + var customResponse = result.CustomResponse as Dictionary; + Assert.NotNull(customResponse); + Assert.Equal(SendAccessConstants.EmailOtpValidatorResults.EmailRequired, customResponse[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_PasswordErrorSelected_HasPassword_ReturnsPasswordDoesNotMatch( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + string password) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid, passwordHash: password); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidRequest, result.Error); + Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, result.ErrorDescription); + + var customResponse = result.CustomResponse as Dictionary; + Assert.NotNull(customResponse); + Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordDoesNotMatch, customResponse[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_PasswordErrorSelected_NoPassword_ReturnsPasswordRequired( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) + { + // Arrange + + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_passwordSendGuid); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + sutProvider.GetDependency().SendDefaultHashKey = _testHashKey; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _passwordSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, result.ErrorDescription); + + var customResponse = result.CustomResponse as Dictionary; + Assert.NotNull(customResponse); + Assert.Equal(SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired, customResponse[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_NullHashKey_UsesEmptyKey( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); + var context = new ExtensionGrantValidationContext { Request = tokenRequest }; + sutProvider.GetDependency().SendDefaultHashKey = null; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_EmptyHashKey_UsesEmptyKey( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(_invalidSendGuid); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency().SendDefaultHashKey = ""; + + // Act + var result = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, _invalidSendGuid); + + // Assert + Assert.True(result.IsError); + Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, result.Error); + Assert.Contains(result.ErrorDescription, SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_ConsistentBehavior_SameSendIdSameResult( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + Guid sendId) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency().SendDefaultHashKey = "consistent-test-key-123456789012345678901234567890"; + + // Act + var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId); + var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId); + + // Assert + Assert.Equal(result1.ErrorDescription, result2.ErrorDescription); + Assert.Equal(result1.Error, result2.Error); + + var customResponse1 = result1.CustomResponse as Dictionary; + var customResponse2 = result2.CustomResponse as Dictionary; + Assert.Equal(customResponse1[SendAccessConstants.SendAccessError], customResponse2[SendAccessConstants.SendAccessError]); + } + + [Theory, BitAutoData] + public async Task ValidateRequestAsync_DifferentSendIds_CanReturnDifferentResults( + SutProvider sutProvider, + [AutoFixture.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + Guid sendId1, + Guid sendId2) + { + // Arrange + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId1); + var context = new ExtensionGrantValidationContext + { + Request = tokenRequest + }; + + sutProvider.GetDependency().SendDefaultHashKey = "different-test-key-123456789012345678901234567890"; + + // Act + var result1 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId1); + var result2 = await sutProvider.Sut.ValidateRequestAsync(context, _authMethod, sendId2); + + // Assert - Both should be errors + Assert.True(result1.IsError); + Assert.True(result2.IsError); + + // Both should have valid error types + var validErrors = new[] + { + SendAccessConstants.SendIdGuidValidatorResults.InvalidSendId, + SendAccessConstants.EmailOtpValidatorResults.EmailRequired, + SendAccessConstants.PasswordValidatorResults.RequestPasswordIsRequired + }; + Assert.Contains(result1.ErrorDescription, validErrors); + Assert.Contains(result2.ErrorDescription, validErrors); + } + + [Fact] + public void Constructor_WithValidGlobalSettings_CreatesInstance() + { + // Arrange + var globalSettings = new Core.Settings.GlobalSettings + { + SendDefaultHashKey = "test-key-123456789012345678901234567890" + }; + + // Act + var validator = new SendNeverAuthenticateRequestValidator(globalSettings); + + // Assert + Assert.NotNull(validator); + } +} diff --git a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs index e77626d37b..460b033fa7 100644 --- a/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/SendAccess/SendPasswordRequestValidatorTests.cs @@ -1,12 +1,7 @@ -using System.Collections.Specialized; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.IdentityServer; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures.SendAccess; -using Bit.Core.Enums; using Bit.Core.KeyManagement.Sends; using Bit.Core.Tools.Models.Data; -using Bit.Core.Utilities; -using Bit.Identity.IdentityServer.Enums; using Bit.Identity.IdentityServer.RequestValidators.SendAccess; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -28,7 +23,7 @@ public class SendPasswordRequestValidatorTests Guid sendId) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId); var context = new ExtensionGrantValidationContext { @@ -58,7 +53,7 @@ public class SendPasswordRequestValidatorTests string clientPasswordHash) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash); var context = new ExtensionGrantValidationContext { @@ -92,7 +87,7 @@ public class SendPasswordRequestValidatorTests string clientPasswordHash) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash); var context = new ExtensionGrantValidationContext { @@ -130,7 +125,7 @@ public class SendPasswordRequestValidatorTests Guid sendId) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, string.Empty); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: string.Empty); var context = new ExtensionGrantValidationContext { @@ -163,7 +158,7 @@ public class SendPasswordRequestValidatorTests { // Arrange var whitespacePassword = " "; - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, whitespacePassword); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: whitespacePassword); var context = new ExtensionGrantValidationContext { @@ -196,7 +191,7 @@ public class SendPasswordRequestValidatorTests // Arrange var firstPassword = "first-password"; var secondPassword = "second-password"; - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, firstPassword, secondPassword); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: [firstPassword, secondPassword]); var context = new ExtensionGrantValidationContext { @@ -229,7 +224,7 @@ public class SendPasswordRequestValidatorTests string clientPasswordHash) { // Arrange - tokenRequest.Raw = CreateValidatedTokenRequest(sendId, clientPasswordHash); + tokenRequest.Raw = SendAccessTestUtilities.CreateValidatedTokenRequest(sendId, passwordHash: clientPasswordHash); var context = new ExtensionGrantValidationContext { @@ -268,30 +263,4 @@ public class SendPasswordRequestValidatorTests // Assert Assert.NotNull(validator); } - - private static NameValueCollection CreateValidatedTokenRequest( - Guid sendId, - params string[] passwordHash) - { - var sendIdBase64 = CoreHelpers.Base64UrlEncode(sendId.ToByteArray()); - - var rawRequestParameters = new NameValueCollection - { - { OidcConstants.TokenRequest.GrantType, CustomGrantTypes.SendAccess }, - { OidcConstants.TokenRequest.ClientId, BitwardenClient.Send }, - { OidcConstants.TokenRequest.Scope, ApiScopes.ApiSendAccess }, - { "device_type", ((int)DeviceType.FirefoxBrowser).ToString() }, - { SendAccessConstants.TokenRequest.SendId, sendIdBase64 } - }; - - if (passwordHash != null && passwordHash.Length > 0) - { - foreach (var hash in passwordHash) - { - rawRequestParameters.Add(SendAccessConstants.TokenRequest.ClientB64HashedPassword, hash); - } - } - - return rawRequestParameters; - } } From 0b4b605524432431485852024dc5b35e16edd001 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 23 Sep 2025 15:52:56 +0000 Subject: [PATCH 268/326] Bumped version to 2025.9.3 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 71303d3529..cd1e95d0bd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.9.2 + 2025.9.3 Bit.$(MSBuildProjectName) enable From 744f11733d838ae1d3a82fdc33ae1fcd368c21c0 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:07:42 -0400 Subject: [PATCH 269/326] Revert "Bumped version to 2025.9.3" (#6369) This reverts commit 0b4b605524432431485852024dc5b35e16edd001. --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index cd1e95d0bd..71303d3529 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.9.3 + 2025.9.2 Bit.$(MSBuildProjectName) enable From ff092a031eb0489672095a76c8f4d4564222b835 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Wed, 24 Sep 2025 05:10:46 +0900 Subject: [PATCH 270/326] [PM-23229] Add extra validation to kdf changes + authentication data + unlock data (#6121) * Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response * Implement support for authentication data and unlock data in kdf change * Extract to kdf command and add tests * Fix namespace * Delete empty file * Fix build * Clean up tests * Fix tests * Add comments * Cleanup * Cleanup * Cleanup * Clean-up and fix build * Address feedback; force new parameters on KDF change request * Clean-up and add tests * Re-add logger * Update logger to interface * Clean up, remove Kdf Request Model * Remove kdf request model tests * Fix types in test * Address feedback to rename request model and re-add tests * Fix namespace * Move comments * Rename InnerKdfRequestModel to KdfRequestModel --------- Co-authored-by: Maciej Zieniuk --- .../Auth/Controllers/AccountsController.cs | 18 +- .../Request/Accounts/KdfRequestModel.cs | 25 -- ...sswordUnlockDataAndAuthenticationModel.cs} | 6 +- .../Request/Accounts/PasswordRequestModel.cs | 14 +- .../Models/Requests/KdfRequestModel.cs | 26 ++ ...rPasswordAuthenticationDataRequestModel.cs | 21 ++ .../MasterPasswordUnlockDataRequestModel.cs | 22 ++ .../Models/Requests/UnlockDataRequestModel.cs | 2 +- src/Core/Entities/User.cs | 5 + .../KeyManagement/Kdf/IChangeKdfCommand.cs | 15 + .../Kdf/Implementations/ChangeKdfCommand.cs | 94 +++++ ...eyManagementServiceCollectionExtensions.cs | 3 + .../KeyManagement/Models/Data/KdfSettings.cs | 38 +++ .../Data/MasterPasswordAuthenticationData.cs | 18 + ...sterPasswordUnlockAndAuthenticationData.cs | 34 ++ .../Models/Data/MasterPasswordUnlockData.cs | 28 +- .../Models/Data/RotateUserAccountKeysData.cs | 2 +- src/Core/Services/IUserService.cs | 2 - .../Services/Implementations/UserService.cs | 33 -- src/Core/Utilities/KdfSettingsValidator.cs | 6 + .../Controllers/AccountsControllerTests.cs | 61 +++- .../Request/MasterPasswordUnlockDataModel.cs | 6 +- .../Request/Accounts/KdfRequestModelTests.cs | 65 ---- .../Utilities/KdfSettingsValidatorTests.cs | 36 ++ .../Kdf/ChangeKdfCommandTests.cs | 322 ++++++++++++++++++ 25 files changed, 729 insertions(+), 173 deletions(-) delete mode 100644 src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs rename src/Api/Auth/Models/Request/Accounts/{MasterPasswordUnlockDataModel.cs => MasterPasswordUnlockDataAndAuthenticationModel.cs} (90%) create mode 100644 src/Api/KeyManagement/Models/Requests/KdfRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs create mode 100644 src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs create mode 100644 src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs create mode 100644 src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs create mode 100644 src/Core/KeyManagement/Models/Data/KdfSettings.cs create mode 100644 src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs create mode 100644 src/Core/KeyManagement/Models/Data/MasterPasswordUnlockAndAuthenticationData.cs delete mode 100644 test/Api.Test/Models/Request/Accounts/KdfRequestModelTests.cs create mode 100644 test/Api.Test/Utilities/KdfSettingsValidatorTests.cs create mode 100644 test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 0bed7c29c4..501399db38 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -16,6 +16,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Kdf; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; @@ -39,7 +40,7 @@ public class AccountsController : Controller private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; private readonly ITwoFactorEmailService _twoFactorEmailService; - + private readonly IChangeKdfCommand _changeKdfCommand; public AccountsController( IOrganizationService organizationService, @@ -51,7 +52,8 @@ public class AccountsController : Controller ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, - ITwoFactorEmailService twoFactorEmailService + ITwoFactorEmailService twoFactorEmailService, + IChangeKdfCommand changeKdfCommand ) { _organizationService = organizationService; @@ -64,7 +66,7 @@ public class AccountsController : Controller _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; _twoFactorEmailService = twoFactorEmailService; - + _changeKdfCommand = changeKdfCommand; } @@ -256,7 +258,7 @@ public class AccountsController : Controller } [HttpPost("kdf")] - public async Task PostKdf([FromBody] KdfRequestModel model) + public async Task PostKdf([FromBody] PasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -264,8 +266,12 @@ public class AccountsController : Controller throw new UnauthorizedAccessException(); } - var result = await _userService.ChangeKdfAsync(user, model.MasterPasswordHash, - model.NewMasterPasswordHash, model.Key, model.Kdf.Value, model.KdfIterations.Value, model.KdfMemory, model.KdfParallelism); + if (model.AuthenticationData == null || model.UnlockData == null) + { + throw new BadRequestException("AuthenticationData and UnlockData must be provided."); + } + + var result = await _changeKdfCommand.ChangeKdfAsync(user, model.MasterPasswordHash, model.AuthenticationData.ToData(), model.UnlockData.ToData()); if (result.Succeeded) { return; diff --git a/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs deleted file mode 100644 index fc62f22bab..0000000000 --- a/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Core.Enums; -using Bit.Core.Utilities; - -namespace Bit.Api.Auth.Models.Request.Accounts; - -public class KdfRequestModel : PasswordRequestModel, IValidatableObject -{ - [Required] - public KdfType? Kdf { get; set; } - [Required] - public int? KdfIterations { get; set; } - public int? KdfMemory { get; set; } - public int? KdfParallelism { get; set; } - - public override IEnumerable Validate(ValidationContext validationContext) - { - if (Kdf.HasValue && KdfIterations.HasValue) - { - return KdfSettingsValidator.Validate(Kdf.Value, KdfIterations.Value, KdfMemory, KdfParallelism); - } - - return Enumerable.Empty(); - } -} diff --git a/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs similarity index 90% rename from src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs rename to src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs index ba57788cec..da361e5a0c 100644 --- a/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs @@ -7,7 +7,7 @@ using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Request.Accounts; -public class MasterPasswordUnlockDataModel : IValidatableObject +public class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject { public required KdfType KdfType { get; set; } public required int KdfIterations { get; set; } @@ -45,9 +45,9 @@ public class MasterPasswordUnlockDataModel : IValidatableObject } } - public MasterPasswordUnlockData ToUnlockData() + public MasterPasswordUnlockAndAuthenticationData ToUnlockData() { - var data = new MasterPasswordUnlockData + var data = new MasterPasswordUnlockAndAuthenticationData { KdfType = KdfType, KdfIterations = KdfIterations, diff --git a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs index 01da1f0f9f..8fa51e9f34 100644 --- a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs @@ -1,7 +1,7 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable +#nullable enable using System.ComponentModel.DataAnnotations; +using Bit.Api.KeyManagement.Models.Requests; namespace Bit.Api.Auth.Models.Request.Accounts; @@ -9,9 +9,13 @@ public class PasswordRequestModel : SecretVerificationRequestModel { [Required] [StringLength(300)] - public string NewMasterPasswordHash { get; set; } + public required string NewMasterPasswordHash { get; set; } [StringLength(50)] - public string MasterPasswordHint { get; set; } + public string? MasterPasswordHint { get; set; } [Required] - public string Key { get; set; } + public required string Key { get; set; } + + // Note: These will eventually become required, but not all consumers are moved over yet. + public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; } + public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; } } diff --git a/src/Api/KeyManagement/Models/Requests/KdfRequestModel.cs b/src/Api/KeyManagement/Models/Requests/KdfRequestModel.cs new file mode 100644 index 0000000000..904304a633 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/KdfRequestModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class KdfRequestModel +{ + [Required] + public required KdfType KdfType { get; init; } + [Required] + public required int Iterations { get; init; } + public int? Memory { get; init; } + public int? Parallelism { get; init; } + + public KdfSettings ToData() + { + return new KdfSettings + { + KdfType = KdfType, + Iterations = Iterations, + Memory = Memory, + Parallelism = Parallelism + }; + } +} diff --git a/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs new file mode 100644 index 0000000000..d65dc8fcb7 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/MasterPasswordAuthenticationDataRequestModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class MasterPasswordAuthenticationDataRequestModel +{ + public required KdfRequestModel Kdf { get; init; } + public required string MasterPasswordAuthenticationHash { get; init; } + [StringLength(256)] public required string Salt { get; init; } + + public MasterPasswordAuthenticationData ToData() + { + return new MasterPasswordAuthenticationData + { + Kdf = Kdf.ToData(), + MasterPasswordAuthenticationHash = MasterPasswordAuthenticationHash, + Salt = Salt + }; + } +} diff --git a/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs new file mode 100644 index 0000000000..ce7a2b343f --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/MasterPasswordUnlockDataRequestModel.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.KeyManagement.Models.Requests; + +public class MasterPasswordUnlockDataRequestModel +{ + public required KdfRequestModel Kdf { get; init; } + [EncryptedString] public required string MasterKeyWrappedUserKey { get; init; } + [StringLength(256)] public required string Salt { get; init; } + + public MasterPasswordUnlockData ToData() + { + return new MasterPasswordUnlockData + { + Kdf = Kdf.ToData(), + MasterKeyWrappedUserKey = MasterKeyWrappedUserKey, + Salt = Salt + }; + } +} diff --git a/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs index 23c3eb95d0..3af944110c 100644 --- a/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs @@ -10,7 +10,7 @@ namespace Bit.Api.KeyManagement.Models.Requests; public class UnlockDataRequestModel { // All methods to get to the userkey - public required MasterPasswordUnlockDataModel MasterPasswordUnlockData { get; set; } + public required MasterPasswordUnlockAndAuthenticationDataModel MasterPasswordUnlockData { get; set; } public required IEnumerable EmergencyAccessUnlockData { get; set; } public required IEnumerable OrganizationAccountRecoveryUnlockData { get; set; } public required IEnumerable PasskeyUnlockData { get; set; } diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index b92d22b0e3..12c527ed78 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -78,6 +78,11 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public DateTime? LastEmailChangeDate { get; set; } public bool VerifyDevices { get; set; } = true; + public string GetMasterPasswordSalt() + { + return Email.ToLowerInvariant().Trim(); + } + public void SetNewId() { Id = CoreHelpers.GenerateComb(); diff --git a/src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs b/src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs new file mode 100644 index 0000000000..081ae5248d --- /dev/null +++ b/src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs @@ -0,0 +1,15 @@ +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.KeyManagement.Kdf; + +/// +/// Command to change the Key Derivation Function (KDF) settings for a user. This includes +/// changing the masterpassword authentication hash, and the masterkey encrypted userkey. +/// The salt must not change during the KDF change. +/// +public interface IChangeKdfCommand +{ + public Task ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData); +} diff --git a/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs new file mode 100644 index 0000000000..fe736f9ac6 --- /dev/null +++ b/src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs @@ -0,0 +1,94 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Platform.Push; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.KeyManagement.Kdf.Implementations; + +/// +public class ChangeKdfCommand : IChangeKdfCommand +{ + private readonly IUserService _userService; + private readonly IPushNotificationService _pushService; + private readonly IUserRepository _userRepository; + private readonly IdentityErrorDescriber _identityErrorDescriber; + private readonly ILogger _logger; + + public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, IUserRepository userRepository, IdentityErrorDescriber describer, ILogger logger) + { + _userService = userService; + _pushService = pushService; + _userRepository = userRepository; + _identityErrorDescriber = describer; + _logger = logger; + } + + public async Task ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData) + { + ArgumentNullException.ThrowIfNull(user); + if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash)) + { + return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); + } + + // 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 + authenticationData.ValidateSaltUnchangedForUser(user); + unlockData.ValidateSaltUnchangedForUser(user); + + // Currently KDF settings are not saved separately for authentication and unlock and must therefore be equal + if (!authenticationData.Kdf.Equals(unlockData.Kdf)) + { + 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."); + } + + // 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. + // The authentication hash, and the unlock data each are dependent on: + // - The master password (entered by the user every time) + // - The KDF settings (iterations, memory, parallelism) + // - The salt + // These combinations - (password, authentication hash, KDF settings, salt) and (password, unlock data, KDF settings, salt) + // must remain consistent to unlock correctly. + + // Authentication + // Note: This mutates the user but does not yet save it to DB. That is done atomically, later. + // 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); + if (!result.Succeeded) + { + _logger.LogWarning("Change KDF failed for user {userId}.", user.Id); + return result; + } + + // Salt is ensured to be the same as authentication data, and the value stored in the account, and is not updated. + // Kdf - These will be seperated in the future, but for now are ensured to be the same as authentication data above. + user.Key = unlockData.MasterKeyWrappedUserKey; + user.Kdf = unlockData.Kdf.KdfType; + user.KdfIterations = unlockData.Kdf.Iterations; + user.KdfMemory = unlockData.Kdf.Memory; + user.KdfParallelism = unlockData.Kdf.Parallelism; + + var now = DateTime.UtcNow; + user.RevisionDate = user.AccountRevisionDate = now; + user.LastKdfChangeDate = now; + + await _userRepository.ReplaceAsync(user); + await _pushService.PushLogOutAsync(user.Id); + return IdentityResult.Success; + } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index cacf3d4140..e4ebdb4860 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using Bit.Core.KeyManagement.Commands; using Bit.Core.KeyManagement.Commands.Interfaces; +using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Kdf.Implementations; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.KeyManagement; @@ -15,5 +17,6 @@ public static class KeyManagementServiceCollectionExtensions private static void AddKeyManagementCommands(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/KeyManagement/Models/Data/KdfSettings.cs b/src/Core/KeyManagement/Models/Data/KdfSettings.cs new file mode 100644 index 0000000000..cc1e465330 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/KdfSettings.cs @@ -0,0 +1,38 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class KdfSettings +{ + public required KdfType KdfType { get; init; } + public required int Iterations { get; init; } + public int? Memory { get; init; } + public int? Parallelism { get; init; } + + public void ValidateUnchangedForUser(User user) + { + if (user.Kdf != KdfType || user.KdfIterations != Iterations || user.KdfMemory != Memory || user.KdfParallelism != Parallelism) + { + throw new ArgumentException("Invalid KDF settings."); + } + } + + public override bool Equals(object? obj) + { + if (obj is not KdfSettings other) + { + return false; + } + + return KdfType == other.KdfType && + Iterations == other.Iterations && + Memory == other.Memory && + Parallelism == other.Parallelism; + } + + public override int GetHashCode() + { + return HashCode.Combine(KdfType, Iterations, Memory, Parallelism); + } +} diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs new file mode 100644 index 0000000000..c0ae949a3f --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordAuthenticationData.cs @@ -0,0 +1,18 @@ +using Bit.Core.Entities; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class MasterPasswordAuthenticationData +{ + public required KdfSettings Kdf { get; init; } + public required string MasterPasswordAuthenticationHash { get; init; } + public required string Salt { get; init; } + + public void ValidateSaltUnchangedForUser(User user) + { + if (user.GetMasterPasswordSalt() != Salt) + { + throw new ArgumentException("Invalid master password salt."); + } + } +} diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockAndAuthenticationData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockAndAuthenticationData.cs new file mode 100644 index 0000000000..e305d92fec --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockAndAuthenticationData.cs @@ -0,0 +1,34 @@ +#nullable enable +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.KeyManagement.Models.Data; + +public class MasterPasswordUnlockAndAuthenticationData +{ + public KdfType KdfType { get; set; } + public int KdfIterations { get; set; } + public int? KdfMemory { get; set; } + public int? KdfParallelism { get; set; } + + public required string Email { get; set; } + public required string MasterKeyAuthenticationHash { get; set; } + public required string MasterKeyEncryptedUserKey { get; set; } + public string? MasterPasswordHint { get; set; } + + public bool ValidateForUser(User user) + { + if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations) + { + return false; + } + else if (Email != user.Email) + { + return false; + } + else + { + return true; + } + } +} diff --git a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs index 0ddfc03190..d1ab6f645b 100644 --- a/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs +++ b/src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs @@ -1,34 +1,20 @@ #nullable enable + using Bit.Core.Entities; -using Bit.Core.Enums; namespace Bit.Core.KeyManagement.Models.Data; public class MasterPasswordUnlockData { - public KdfType KdfType { get; set; } - public int KdfIterations { get; set; } - public int? KdfMemory { get; set; } - public int? KdfParallelism { get; set; } + public required KdfSettings Kdf { get; init; } + public required string MasterKeyWrappedUserKey { get; init; } + public required string Salt { get; init; } - public required string Email { get; set; } - public required string MasterKeyAuthenticationHash { get; set; } - public required string MasterKeyEncryptedUserKey { get; set; } - public string? MasterPasswordHint { get; set; } - - public bool ValidateForUser(User user) + public void ValidateSaltUnchangedForUser(User user) { - if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations) + if (user.GetMasterPasswordSalt() != Salt) { - return false; - } - else if (Email != user.Email) - { - return false; - } - else - { - return true; + throw new ArgumentException("Invalid master password salt."); } } } diff --git a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs index b89f19797f..557fb56ff3 100644 --- a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs @@ -19,7 +19,7 @@ public class RotateUserAccountKeysData public string AccountPublicKey { get; set; } // All methods to get to the userkey - public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; } + public MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; } public IEnumerable EmergencyAccesses { get; set; } public IReadOnlyList OrganizationUsers { get; set; } public IEnumerable WebAuthnKeys { get; set; } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index ef602be93a..412f9db36e 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -38,8 +38,6 @@ public interface IUserService Task ConvertToKeyConnectorAsync(User user); Task AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key); Task UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint); - Task ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key, - KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true); Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 386cb8c3d2..a36b9e37cc 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -777,39 +777,6 @@ public class UserService : UserManager, IUserService return IdentityResult.Success; } - public async Task ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, - string key, KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - if (await CheckPasswordAsync(user, masterPassword)) - { - var result = await UpdatePasswordHash(user, newMasterPassword); - if (!result.Succeeded) - { - return result; - } - - var now = DateTime.UtcNow; - user.RevisionDate = user.AccountRevisionDate = now; - user.LastKdfChangeDate = now; - user.Key = key; - user.Kdf = kdf; - user.KdfIterations = kdfIterations; - user.KdfMemory = kdfMemory; - user.KdfParallelism = kdfParallelism; - await _userRepository.ReplaceAsync(user); - await _pushService.PushLogOutAsync(user.Id); - return IdentityResult.Success; - } - - Logger.LogWarning("Change KDF failed for user {userId}.", user.Id); - return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); - } - public async Task RefreshSecurityStampAsync(User user, string secret) { if (user == null) diff --git a/src/Core/Utilities/KdfSettingsValidator.cs b/src/Core/Utilities/KdfSettingsValidator.cs index db7936acff..f89e8ddb66 100644 --- a/src/Core/Utilities/KdfSettingsValidator.cs +++ b/src/Core/Utilities/KdfSettingsValidator.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.Utilities; @@ -34,4 +35,9 @@ public static class KdfSettingsValidator break; } } + + public static IEnumerable Validate(KdfSettings settings) + { + return Validate(settings.KdfType, settings.Iterations, settings.Memory, settings.Parallelism); + } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index ce870c0860..e81d51281d 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -10,6 +10,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Kdf; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture.Attributes; @@ -33,6 +34,7 @@ public class AccountsControllerTests : IDisposable private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; private readonly ITwoFactorEmailService _twoFactorEmailService; + private readonly IChangeKdfCommand _changeKdfCommand; public AccountsControllerTests() @@ -47,7 +49,7 @@ public class AccountsControllerTests : IDisposable _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); _twoFactorEmailService = Substitute.For(); - + _changeKdfCommand = Substitute.For(); _sut = new AccountsController( _organizationService, @@ -59,7 +61,8 @@ public class AccountsControllerTests : IDisposable _tdeOffboardingPasswordCommand, _twoFactorIsEnabledQuery, _featureService, - _twoFactorEmailService + _twoFactorEmailService, + _changeKdfCommand ); } @@ -242,12 +245,18 @@ public class AccountsControllerTests : IDisposable { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); - _userService.ChangePasswordAsync(user, default, default, default, default) + _userService.ChangePasswordAsync(user, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(IdentityResult.Success)); - await _sut.PostPassword(new PasswordRequestModel()); + await _sut.PostPassword(new PasswordRequestModel + { + MasterPasswordHash = "masterPasswordHash", + NewMasterPasswordHash = "newMasterPasswordHash", + MasterPasswordHint = "masterPasswordHint", + Key = "key" + }); - await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default); + await _userService.Received(1).ChangePasswordAsync(user, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] @@ -256,7 +265,13 @@ public class AccountsControllerTests : IDisposable ConfigureUserServiceToReturnNullPrincipal(); await Assert.ThrowsAsync( - () => _sut.PostPassword(new PasswordRequestModel()) + () => _sut.PostPassword(new PasswordRequestModel + { + MasterPasswordHash = "masterPasswordHash", + NewMasterPasswordHash = "newMasterPasswordHash", + MasterPasswordHint = "masterPasswordHint", + Key = "key" + }) ); } @@ -265,11 +280,17 @@ public class AccountsControllerTests : IDisposable { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); - _userService.ChangePasswordAsync(user, default, default, default, default) + _userService.ChangePasswordAsync(user, Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromResult(IdentityResult.Failed())); await Assert.ThrowsAsync( - () => _sut.PostPassword(new PasswordRequestModel()) + () => _sut.PostPassword(new PasswordRequestModel + { + MasterPasswordHash = "masterPasswordHash", + NewMasterPasswordHash = "newMasterPasswordHash", + MasterPasswordHint = "masterPasswordHint", + Key = "key" + }) ); } @@ -593,6 +614,30 @@ public class AccountsControllerTests : IDisposable await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user); } + [Theory] + [BitAutoData] + public async Task PostKdf_WithNullAuthenticationData_ShouldFail( + User user, PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + model.AuthenticationData = null; + + // Act + await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + } + + [Theory] + [BitAutoData] + public async Task PostKdf_WithNullUnlockData_ShouldFail( + User user, PasswordRequestModel model) + { + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(Task.FromResult(user)); + model.UnlockData = null; + + // Act + await Assert.ThrowsAsync(() => _sut.PostKdf(model)); + } + // Below are helper functions that currently belong to this // test class, but ultimately may need to be split out into // something greater in order to share common test steps with diff --git a/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs b/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs index 4c78c7015a..254f57a128 100644 --- a/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs +++ b/test/Api.Test/KeyManagement/Models/Request/MasterPasswordUnlockDataModel.cs @@ -18,7 +18,7 @@ public class MasterPasswordUnlockDataModelTests [InlineData(KdfType.Argon2id, 3, 64, 4)] public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) { - var model = new MasterPasswordUnlockDataModel + var model = new MasterPasswordUnlockAndAuthenticationDataModel { KdfType = kdfType, KdfIterations = kdfIterations, @@ -43,7 +43,7 @@ public class MasterPasswordUnlockDataModelTests [InlineData((KdfType)2, 2, 64, 4)] public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) { - var model = new MasterPasswordUnlockDataModel + var model = new MasterPasswordUnlockAndAuthenticationDataModel { KdfType = kdfType, KdfIterations = kdfIterations, @@ -59,7 +59,7 @@ public class MasterPasswordUnlockDataModelTests Assert.NotNull(result.First().ErrorMessage); } - private static List Validate(MasterPasswordUnlockDataModel model) + private static List Validate(MasterPasswordUnlockAndAuthenticationDataModel model) { var results = new List(); Validator.TryValidateObject(model, new ValidationContext(model), results, true); diff --git a/test/Api.Test/Models/Request/Accounts/KdfRequestModelTests.cs b/test/Api.Test/Models/Request/Accounts/KdfRequestModelTests.cs deleted file mode 100644 index 612b7ad442..0000000000 --- a/test/Api.Test/Models/Request/Accounts/KdfRequestModelTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Core.Enums; -using Xunit; - -namespace Bit.Api.Test.Models.Request.Accounts; - -public class KdfRequestModelTests -{ - [Theory] - [InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle - [InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary - [InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary - [InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle - [InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary - [InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary - public void Validate_IsValid(KdfType kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism) - { - var model = new KdfRequestModel - { - Kdf = kdfType, - KdfIterations = kdfIterations, - KdfMemory = kdfMemory, - KdfParallelism = kdfParallelism, - Key = "TEST", - NewMasterPasswordHash = "TEST", - }; - - var results = Validate(model); - Assert.Empty(results); - } - - [Theory] - [InlineData(null, 350_000, null, null, 1)] // Although KdfType is nullable, it's marked as [Required] - [InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations - [InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations - [InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0 - [InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory - [InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value - [InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory - [InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value - public void Validate_Fails(KdfType? kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures) - { - var model = new KdfRequestModel - { - Kdf = kdfType, - KdfIterations = kdfIterations, - KdfMemory = kdfMemory, - KdfParallelism = kdfParallelism, - Key = "TEST", - NewMasterPasswordHash = "TEST", - }; - - var results = Validate(model); - Assert.NotEmpty(results); - Assert.Equal(expectedFailures, results.Count); - } - - public static List Validate(KdfRequestModel model) - { - var results = new List(); - Validator.TryValidateObject(model, new ValidationContext(model), results); - return results; - } -} diff --git a/test/Api.Test/Utilities/KdfSettingsValidatorTests.cs b/test/Api.Test/Utilities/KdfSettingsValidatorTests.cs new file mode 100644 index 0000000000..5c556979c7 --- /dev/null +++ b/test/Api.Test/Utilities/KdfSettingsValidatorTests.cs @@ -0,0 +1,36 @@ +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Xunit; + +namespace Bit.Api.Test.Utilities; + +public class KdfSettingsValidatorTests +{ + [Theory] + [InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle + [InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary + [InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary + [InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle + [InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary + [InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary + public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) + { + var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism); + Assert.Empty(results); + } + + [Theory] + [InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations + [InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations + [InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0 + [InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory + [InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value + [InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory + [InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value + public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures) + { + var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism); + Assert.NotEmpty(results); + Assert.Equal(expectedFailures, results.Count()); + } +} diff --git a/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs new file mode 100644 index 0000000000..02e04b9ce9 --- /dev/null +++ b/test/Core.Test/KeyManagement/Kdf/ChangeKdfCommandTests.cs @@ -0,0 +1,322 @@ +#nullable enable + +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Kdf.Implementations; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Kdf; + +[SutProviderCustomize] +public class ChangeKdfCommandTests +{ + [Theory] + [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)); + + var kdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 4, + Memory = 512, + Parallelism = 4 + }; + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "newMasterPassword", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", + Salt = user.GetMasterPasswordSalt() + }; + + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is(u => + u.Id == user.Id + && u.Kdf == Enums.KdfType.Argon2id + && u.KdfIterations == 4 + && u.KdfMemory == 512 + && u.KdfParallelism == 4 + )); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider sutProvider) + { + var kdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 4, + Memory = 512, + Parallelism = 4 + }; + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "newMasterPassword", + Salt = "salt" + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", + Salt = "salt" + }; + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ChangeKdfAsync(null!, "masterPassword", authenticationData, unlockData)); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider sutProvider, User user) + { + 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 authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "newMasterPassword", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", + Salt = user.GetMasterPasswordSalt() + }; + + var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); + Assert.False(result.Succeeded); + Assert.Contains(result.Errors, e => e.Code == "PasswordMismatch"); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider sutProvider, User user) + { + 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()).Returns(Task.FromResult(IdentityResult.Success)); + + 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" + )); + } + + [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 }, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = new KdfSettings { KdfType = Enums.KdfType.PBKDF2_SHA256, Iterations = 100000 }, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = user.GetMasterPasswordSalt() + }; + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider sutProvider, User user, KdfSettings kdf) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = "different-salt" + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = user.GetMasterPasswordSalt() + }; + 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) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = "different-salt" + }; + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider sutProvider, User user) + { + 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)); + + var kdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 4, + Memory = 512, + Parallelism = 4 + }; + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = kdf, + MasterPasswordAuthenticationHash = "newMasterPassword", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = kdf, + MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", + Salt = user.GetMasterPasswordSalt() + }; + + var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); + + Assert.False(result.Succeeded); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider sutProvider, User user) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + + // Create invalid KDF settings (iterations too low for PBKDF2) + var invalidKdf = new KdfSettings + { + KdfType = Enums.KdfType.PBKDF2_SHA256, + Iterations = 1000, // This is below the minimum of 600,000 + Memory = null, + Parallelism = null + }; + + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = invalidKdf, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = invalidKdf, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = user.GetMasterPasswordSalt() + }; + + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); + + Assert.Equal("KDF settings are invalid.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider sutProvider, User user) + { + sutProvider.GetDependency().CheckPasswordAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(true)); + + // Create invalid Argon2 KDF settings (memory too high) + var invalidKdf = new KdfSettings + { + KdfType = Enums.KdfType.Argon2id, + Iterations = 3, // Valid + Memory = 2048, // This is above the maximum of 1024 + Parallelism = 4 // Valid + }; + + var authenticationData = new MasterPasswordAuthenticationData + { + Kdf = invalidKdf, + MasterPasswordAuthenticationHash = "new-auth-hash", + Salt = user.GetMasterPasswordSalt() + }; + var unlockData = new MasterPasswordUnlockData + { + Kdf = invalidKdf, + MasterKeyWrappedUserKey = "new-wrapped-key", + Salt = user.GetMasterPasswordSalt() + }; + + var exception = await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); + + Assert.Equal("KDF settings are invalid.", exception.Message); + } + +} From 6e4f05ebd388e9a1dc9ec8103d070b516b65c1ae Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 24 Sep 2025 08:42:56 -0400 Subject: [PATCH 271/326] fix: change policies to static strings and update auth owned endpoints (#6296) --- src/Api/Auth/Controllers/AccountsController.cs | 3 ++- .../Auth/Controllers/AuthRequestsController.cs | 3 ++- .../Controllers/EmergencyAccessController.cs | 2 +- src/Api/Auth/Controllers/TwoFactorController.cs | 3 ++- src/Api/Auth/Controllers/WebAuthnController.cs | 3 ++- src/Api/Startup.cs | 17 +++++++++-------- src/Core/Auth/Identity/Policies.cs | 8 +++++++- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 501399db38..19165a5a1c 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -9,6 +9,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; @@ -27,7 +28,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("accounts")] -[Authorize("Application")] +[Authorize(Policies.Application)] public class AccountsController : Controller { private readonly IOrganizationService _organizationService; diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index e4a9027f20..4da3a2f491 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -5,6 +5,7 @@ using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Request.AuthRequest; using Bit.Core.Auth.Services; using Bit.Core.Exceptions; @@ -18,7 +19,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("auth-requests")] -[Authorize("Application")] +[Authorize(Policies.Application)] public class AuthRequestsController( IUserService userService, IAuthRequestRepository authRequestRepository, diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index b849dc3e07..016cd82fe2 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -18,7 +18,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("emergency-access")] -[Authorize("Application")] +[Authorize(Core.Auth.Identity.Policies.Application)] public class EmergencyAccessController : Controller { private readonly IUserService _userService; diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 886ed2cd20..0af46fb57c 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Response.TwoFactor; using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; @@ -26,7 +27,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("two-factor")] -[Authorize("Web")] +[Authorize(Policies.Web)] public class TwoFactorController : Controller { private readonly IUserService _userService; diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index bb17607954..60b8621c5e 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; @@ -20,7 +21,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Auth.Controllers; [Route("webauthn")] -[Authorize("Web")] +[Authorize(Policies.Web)] public class WebAuthnController : Controller { private readonly IUserService _userService; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 1d5a1609f4..cc50a1b362 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -34,6 +34,7 @@ using Bit.Core.Dirt.Reports.ReportFeatures; using Bit.Core.Tools.SendFeatures; using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.Identity; +using Bit.Core.Enums; #if !OSS @@ -105,40 +106,40 @@ public class Startup services.AddCustomIdentityServices(globalSettings); services.AddIdentityAuthenticationServices(globalSettings, Environment, config => { - config.AddPolicy("Application", policy => + config.AddPolicy(Policies.Application, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external"); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api); }); - config.AddPolicy("Web", policy => + config.AddPolicy(Policies.Web, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external"); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api); - policy.RequireClaim(JwtClaimTypes.ClientId, "web"); + policy.RequireClaim(JwtClaimTypes.ClientId, BitwardenClient.Web); }); - config.AddPolicy("Push", policy => + config.AddPolicy(Policies.Push, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiPush); }); - config.AddPolicy("Licensing", policy => + config.AddPolicy(Policies.Licensing, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiLicensing); }); - config.AddPolicy("Organization", policy => + config.AddPolicy(Policies.Organization, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiOrganization); }); - config.AddPolicy("Installation", policy => + config.AddPolicy(Policies.Installation, policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiInstallation); }); - config.AddPolicy("Secrets", policy => + config.AddPolicy(Policies.Secrets, policy => { policy.RequireAuthenticatedUser(); policy.RequireAssertion(ctx => ctx.User.HasClaim(c => diff --git a/src/Core/Auth/Identity/Policies.cs b/src/Core/Auth/Identity/Policies.cs index 78d86d06a4..b2d94b0a6e 100644 --- a/src/Core/Auth/Identity/Policies.cs +++ b/src/Core/Auth/Identity/Policies.cs @@ -6,5 +6,11 @@ public static class Policies /// Policy for managing access to the Send feature. /// public const string Send = "Send"; // [Authorize(Policy = Policies.Send)] - // TODO: migrate other existing policies to use this class + public const string Application = "Application"; // [Authorize(Policy = Policies.Application)] + public const string Web = "Web"; // [Authorize(Policy = Policies.Web)] + public const string Push = "Push"; // [Authorize(Policy = Policies.Push)] + public const string Licensing = "Licensing"; // [Authorize(Policy = Policies.Licensing)] + public const string Organization = "Organization"; // [Authorize(Policy = Policies.Organization)] + public const string Installation = "Installation"; // [Authorize(Policy = Policies.Installation)] + public const string Secrets = "Secrets"; // [Authorize(Policy = Policies.Secrets)] } From 6edab46d979f3bc28c733a8f9cb2bfbee0251cee Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:52:04 -0500 Subject: [PATCH 272/326] [PM-24357] Do not purge ciphers in the default collection (#6320) * do not purge ciphers in the default collection * Update `DeleteByOrganizationId` procedure to be more performant based on PR review feedback * update EF integration for purge to match new SQL implementation * update Cipher_DeleteByOrganizationId based on PR feedback from dbops team --- .../Vault/Repositories/CipherRepository.cs | 23 +-- .../Cipher/Cipher_DeleteByOrganizationId.sql | 120 ++++++++----- .../Repositories/CipherRepositoryTests.cs | 160 ++++++++++++++++++ ...9-22_00_ExcludeDefaultCiphersFromPurge.sql | 91 ++++++++++ 4 files changed, 345 insertions(+), 49 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-09-22_00_ExcludeDefaultCiphersFromPurge.sql diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 4b2d09f87b..d88f0e98bb 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -281,17 +281,20 @@ public class CipherRepository : Repository + cc.Collection.Type == CollectionType.DefaultUserCollection) + select c; + dbContext.RemoveRange(ciphersToDelete); - var ciphers = from c in dbContext.Ciphers - where c.OrganizationId == organizationId - select c; - dbContext.RemoveRange(ciphers); + var collectionCiphersToRemove = from cc in dbContext.CollectionCiphers + join col in dbContext.Collections on cc.CollectionId equals col.Id + join c in dbContext.Ciphers on cc.CipherId equals c.Id + where col.Type != CollectionType.DefaultUserCollection + && c.OrganizationId == organizationId + select cc; + dbContext.RemoveRange(collectionCiphersToRemove); await OrganizationUpdateStorage(organizationId); await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId); diff --git a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql index d2324a1d00..c14a612b0f 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql @@ -1,49 +1,91 @@ CREATE PROCEDURE [dbo].[Cipher_DeleteByOrganizationId] - @OrganizationId AS UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON + @OrganizationId UNIQUEIDENTIFIER + AS + BEGIN + SET NOCOUNT ON; - DECLARE @BatchSize INT = 100 + DECLARE @BatchSize INT = 1000; - -- Delete collection ciphers - WHILE @BatchSize > 0 - BEGIN - BEGIN TRANSACTION Cipher_DeleteByOrganizationId_CC + BEGIN TRY + BEGIN TRANSACTION; - DELETE TOP(@BatchSize) CC - FROM - [dbo].[CollectionCipher] CC - INNER JOIN - [dbo].[Collection] C ON C.[Id] = CC.[CollectionId] - WHERE - C.[OrganizationId] = @OrganizationId + --------------------------------------------------------------------- + -- 1. Delete organization ciphers that are NOT in any default + -- user collection (Collection.Type = 1). + --------------------------------------------------------------------- + WHILE 1 = 1 + BEGIN + ;WITH Target AS + ( + SELECT TOP (@BatchSize) C.Id + FROM dbo.Cipher C + WHERE C.OrganizationId = @OrganizationId + AND NOT EXISTS ( + SELECT 1 + FROM dbo.CollectionCipher CC2 + INNER JOIN dbo.Collection Col2 + ON Col2.Id = CC2.CollectionId + AND Col2.Type = 1 -- Default user collection + WHERE CC2.CipherId = C.Id + ) + ORDER BY C.Id -- Deterministic ordering (matches clustered index) + ) + DELETE C + FROM dbo.Cipher C + INNER JOIN Target T ON T.Id = C.Id; - SET @BatchSize = @@ROWCOUNT + IF @@ROWCOUNT = 0 BREAK; + END - COMMIT TRANSACTION Cipher_DeleteByOrganizationId_CC - END + --------------------------------------------------------------------- + -- 2. Remove remaining CollectionCipher rows that reference + -- non-default (Type = 0 / shared) collections, for ciphers + -- that were preserved because they belong to at least one + -- default (Type = 1) collection. + --------------------------------------------------------------------- + SET @BatchSize = 1000; + WHILE 1 = 1 + BEGIN + ;WITH ToDelete AS + ( + SELECT TOP (@BatchSize) + CC.CipherId, + CC.CollectionId + FROM dbo.CollectionCipher CC + INNER JOIN dbo.Collection Col + ON Col.Id = CC.CollectionId + AND Col.Type = 0 -- Non-default collections + INNER JOIN dbo.Cipher C + ON C.Id = CC.CipherId + WHERE C.OrganizationId = @OrganizationId + ORDER BY CC.CollectionId, CC.CipherId -- Matches clustered index + ) + DELETE CC + FROM dbo.CollectionCipher CC + INNER JOIN ToDelete TD + ON CC.CipherId = TD.CipherId + AND CC.CollectionId = TD.CollectionId; - -- Reset batch size - SET @BatchSize = 100 + IF @@ROWCOUNT = 0 BREAK; + END - -- Delete ciphers - WHILE @BatchSize > 0 - BEGIN - BEGIN TRANSACTION Cipher_DeleteByOrganizationId + --------------------------------------------------------------------- + -- 3. Bump revision date (inside transaction for consistency) + --------------------------------------------------------------------- + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId; - DELETE TOP(@BatchSize) - FROM - [dbo].[Cipher] - WHERE - [OrganizationId] = @OrganizationId + COMMIT TRANSACTION ; - SET @BatchSize = @@ROWCOUNT - - COMMIT TRANSACTION Cipher_DeleteByOrganizationId - END - - -- Cleanup organization - EXEC [dbo].[Organization_UpdateStorage] @OrganizationId - EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId -END \ No newline at end of file + --------------------------------------------------------------------- + -- 4. Update storage usage (outside the transaction to avoid + -- holding locks during long-running calculation) + --------------------------------------------------------------------- + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH + END + GO diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index ef28d776d7..ee8cd0247d 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -1241,4 +1241,164 @@ public class CipherRepositoryTests Assert.NotNull(archivedCipher); Assert.NotNull(archivedCipher.ArchivedDate); } + + [DatabaseTheory, DatabaseData] + public async Task DeleteByOrganizationIdAsync_ExcludesDefaultCollectionCiphers( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + ICipherRepository cipherRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = user.Email, + Plan = "Test" + }); + + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Default Collection", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }); + + var sharedCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Shared Collection", + OrganizationId = organization.Id, + }); + + async Task CreateOrgCipherAsync() => await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var cipherInDefaultCollection = await CreateOrgCipherAsync(); + var cipherInSharedCollection = await CreateOrgCipherAsync(); + var cipherInBothCollections = await CreateOrgCipherAsync(); + var unassignedCipher = await CreateOrgCipherAsync(); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInDefaultCollection.Id, organization.Id, + new List { defaultCollection.Id }); + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection.Id, organization.Id, + new List { sharedCollection.Id }); + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInBothCollections.Id, organization.Id, + new List { defaultCollection.Id, sharedCollection.Id }); + + await cipherRepository.DeleteByOrganizationIdAsync(organization.Id); + + var remainingCipherInDefault = await cipherRepository.GetByIdAsync(cipherInDefaultCollection.Id); + var deletedCipherInShared = await cipherRepository.GetByIdAsync(cipherInSharedCollection.Id); + var remainingCipherInBoth = await cipherRepository.GetByIdAsync(cipherInBothCollections.Id); + var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id); + + Assert.Null(deletedCipherInShared); + Assert.Null(deletedUnassignedCipher); + + Assert.NotNull(remainingCipherInDefault); + Assert.NotNull(remainingCipherInBoth); + + var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id); + + // Should still have the default collection cipher relationships + Assert.Contains(remainingCollectionCiphers, cc => + cc.CipherId == cipherInDefaultCollection.Id && cc.CollectionId == defaultCollection.Id); + Assert.Contains(remainingCollectionCiphers, cc => + cc.CipherId == cipherInBothCollections.Id && cc.CollectionId == defaultCollection.Id); + + // Should not have the shared collection cipher relationships + Assert.DoesNotContain(remainingCollectionCiphers, cc => cc.CollectionId == sharedCollection.Id); + } + + [DatabaseTheory, DatabaseData] + public async Task DeleteByOrganizationIdAsync_DeletesAllWhenNoDefaultCollections( + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + ICipherRepository cipherRepository, + ICollectionRepository collectionRepository, + ICollectionCipherRepository collectionCipherRepository) + { + // Arrange + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Organization", + BillingEmail = user.Email, + Plan = "Test" + }); + + var sharedCollection1 = await collectionRepository.CreateAsync(new Collection + { + Name = "Shared Collection 1", + OrganizationId = organization.Id, + Type = CollectionType.SharedCollection + }); + + var sharedCollection2 = await collectionRepository.CreateAsync(new Collection + { + Name = "Shared Collection 2", + OrganizationId = organization.Id, + Type = CollectionType.SharedCollection + }); + + // Create ciphers + var cipherInSharedCollection1 = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var cipherInSharedCollection2 = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var unassignedCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection1.Id, organization.Id, + new List { sharedCollection1.Id }); + await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection2.Id, organization.Id, + new List { sharedCollection2.Id }); + + await cipherRepository.DeleteByOrganizationIdAsync(organization.Id); + + var deletedCipher1 = await cipherRepository.GetByIdAsync(cipherInSharedCollection1.Id); + var deletedCipher2 = await cipherRepository.GetByIdAsync(cipherInSharedCollection2.Id); + var deletedUnassignedCipher = await cipherRepository.GetByIdAsync(unassignedCipher.Id); + + Assert.Null(deletedCipher1); + Assert.Null(deletedCipher2); + Assert.Null(deletedUnassignedCipher); + + // All collection cipher relationships should be removed + var remainingCollectionCiphers = await collectionCipherRepository.GetManyByOrganizationIdAsync(organization.Id); + Assert.Empty(remainingCollectionCiphers); + } } diff --git a/util/Migrator/DbScripts/2025-09-22_00_ExcludeDefaultCiphersFromPurge.sql b/util/Migrator/DbScripts/2025-09-22_00_ExcludeDefaultCiphersFromPurge.sql new file mode 100644 index 0000000000..8eb37a570f --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-22_00_ExcludeDefaultCiphersFromPurge.sql @@ -0,0 +1,91 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER + AS + BEGIN + SET NOCOUNT ON; + + DECLARE @BatchSize INT = 1000; + + BEGIN TRY + BEGIN TRANSACTION; + + --------------------------------------------------------------------- + -- 1. Delete organization ciphers that are NOT in any default + -- user collection (Collection.Type = 1). + --------------------------------------------------------------------- + WHILE 1 = 1 + BEGIN + ;WITH Target AS + ( + SELECT TOP (@BatchSize) C.Id + FROM dbo.Cipher C + WHERE C.OrganizationId = @OrganizationId + AND NOT EXISTS ( + SELECT 1 + FROM dbo.CollectionCipher CC2 + INNER JOIN dbo.Collection Col2 + ON Col2.Id = CC2.CollectionId + AND Col2.Type = 1 -- Default user collection + WHERE CC2.CipherId = C.Id + ) + ORDER BY C.Id -- Deterministic ordering (matches clustered index) + ) + DELETE C + FROM dbo.Cipher C + INNER JOIN Target T ON T.Id = C.Id; + + IF @@ROWCOUNT = 0 BREAK; + END + + --------------------------------------------------------------------- + -- 2. Remove remaining CollectionCipher rows that reference + -- non-default (Type = 0 / shared) collections, for ciphers + -- that were preserved because they belong to at least one + -- default (Type = 1) collection. + --------------------------------------------------------------------- + SET @BatchSize = 1000; + WHILE 1 = 1 + BEGIN + ;WITH ToDelete AS + ( + SELECT TOP (@BatchSize) + CC.CipherId, + CC.CollectionId + FROM dbo.CollectionCipher CC + INNER JOIN dbo.Collection Col + ON Col.Id = CC.CollectionId + AND Col.Type = 0 -- Non-default collections + INNER JOIN dbo.Cipher C + ON C.Id = CC.CipherId + WHERE C.OrganizationId = @OrganizationId + ORDER BY CC.CollectionId, CC.CipherId -- Matches clustered index + ) + DELETE CC + FROM dbo.CollectionCipher CC + INNER JOIN ToDelete TD + ON CC.CipherId = TD.CipherId + AND CC.CollectionId = TD.CollectionId; + + IF @@ROWCOUNT = 0 BREAK; + END + + --------------------------------------------------------------------- + -- 3. Bump revision date (inside transaction for consistency) + --------------------------------------------------------------------- + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId; + + COMMIT TRANSACTION ; + + --------------------------------------------------------------------- + -- 4. Update storage usage (outside the transaction to avoid + -- holding locks during long-running calculation) + --------------------------------------------------------------------- + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId; + END TRY + BEGIN CATCH + IF @@TRANCOUNT > 0 + ROLLBACK TRANSACTION; + THROW; + END CATCH + END + GO From 68f7e8c15c4284ee26ef5121ee935d2f525e3b3c Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:30:43 -0400 Subject: [PATCH 273/326] chore(feature-flag) Added feature flag for pm-22110-disable-alternate-login-methods --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 96ee509db1..5c00db9da4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -144,6 +144,7 @@ public static class FeatureFlagKeys public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string Otp6Digits = "pm-18612-otp-6-digits"; public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email"; + public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; From 4b10c1641989c6e65b299d3343d46b499cdf44fd Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Wed, 24 Sep 2025 18:23:15 -0400 Subject: [PATCH 274/326] fix(global-settings): [PM-26092] Token Refresh Doc Enhancement (#6367) * fix(global-settings): [PM-26092] Token Refresh Doc Enhancement - Enhanced documentation and wording for token refresh. --- src/Core/Settings/GlobalSettings.cs | 25 ++++++++++++++++++++---- src/Identity/IdentityServer/ApiClient.cs | 2 +- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 107fd29236..546e668093 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -473,17 +473,34 @@ public class GlobalSettings : IGlobalSettings public string CosmosConnectionString { get; set; } public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzM0NTY2NDAwLCJleHAiOjE3NjQ5NzkyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiNjg3OCIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwicHJvZHVjdCI6IkJpdHdhcmRlbiJ9.TYc88W_t2t0F2AJV3rdyKwGyQKrKFriSAzm1tWFNHNR9QizfC-8bliGdT4Wgeie-ynCXs9wWaF-sKC5emg--qS7oe2iIt67Qd88WS53AwgTvAddQRA4NhGB1R7VM8GAikLieSos-DzzwLYRgjZdmcsprItYGSJuY73r-7-F97ta915majBytVxGF966tT9zF1aYk0bA8FS6DcDYkr5f7Nsy8daS_uIUAgNa_agKXtmQPqKujqtUb6rgWEpSp4OcQcG-8Dpd5jHqoIjouGvY-5LTgk5WmLxi_m-1QISjxUJrUm-UGao3_VwV5KFGqYrz8csdTl-HS40ihWcsWnrV0ug"; /// - /// Global override for sliding refresh token lifetime in seconds. If null, uses the constructor parameter value. + /// Sliding lifetime of a refresh token in seconds. + /// + /// Each time the refresh token is used before the sliding window ends, its lifetime is extended by another SlidingRefreshTokenLifetimeSeconds. + /// + /// If AbsoluteRefreshTokenLifetimeSeconds > 0, the sliding extensions are bounded by the absolute maximum lifetime. + /// If SlidingRefreshTokenLifetimeSeconds = 0, sliding mode is invalid (refresh tokens cannot be used). /// public int? SlidingRefreshTokenLifetimeSeconds { get; set; } /// - /// Global override for absolute refresh token lifetime in seconds. If null, uses the constructor parameter value. + /// Maximum lifetime of a refresh token in seconds. + /// + /// Token cannot be refreshed by any means beyond the absolute refresh expiration. + /// + /// When setting this value to 0, the following effect applies: + /// If ApplyAbsoluteExpirationOnRefreshToken is set to true, the behavior is the same as when no refresh tokens are used. + /// If ApplyAbsoluteExpirationOnRefreshToken is set to false, refresh tokens only expire after the SlidingRefreshTokenLifetimeSeconds has passed. /// public int? AbsoluteRefreshTokenLifetimeSeconds { get; set; } /// - /// Global override for refresh token expiration policy. False = Sliding (default), True = Absolute. + /// Controls whether refresh tokens expire absolutely or on a sliding window basis. + /// + /// Absolute: + /// Token expires at a fixed point in time (defined by AbsoluteRefreshTokenLifetimeSeconds). Usage does not extend lifetime. + /// + /// Sliding(default): + /// Token lifetime is renewed on each use, by the amount in SlidingRefreshTokenLifetimeSeconds. Extensions stop once AbsoluteRefreshTokenLifetimeSeconds is reached (if set > 0). /// - public bool UseAbsoluteRefreshTokenExpiration { get; set; } = false; + public bool ApplyAbsoluteExpirationOnRefreshToken { get; set; } = false; } public class DataProtectionSettings diff --git a/src/Identity/IdentityServer/ApiClient.cs b/src/Identity/IdentityServer/ApiClient.cs index 61b51797c0..ead19813ec 100644 --- a/src/Identity/IdentityServer/ApiClient.cs +++ b/src/Identity/IdentityServer/ApiClient.cs @@ -20,7 +20,7 @@ public class ApiClient : Client AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode, WebAuthnGrantValidator.GrantType }; // Use global setting: false = Sliding (default), true = Absolute - RefreshTokenExpiration = globalSettings.IdentityServer.UseAbsoluteRefreshTokenExpiration + RefreshTokenExpiration = globalSettings.IdentityServer.ApplyAbsoluteExpirationOnRefreshToken ? TokenExpiration.Absolute : TokenExpiration.Sliding; From f0953ed6b0007af0168993a57f2c5f601c90d16c Mon Sep 17 00:00:00 2001 From: Nik Gilmore Date: Wed, 24 Sep 2025 15:25:40 -0700 Subject: [PATCH 275/326] [PM-26126] Add includeMemberItems query param to GET /organization-details (#6376) --- src/Api/Vault/Controllers/CiphersController.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 6249e264c0..c0a974bce2 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -336,13 +336,15 @@ public class CiphersController : Controller } [HttpGet("organization-details")] - public async Task> GetOrganizationCiphers(Guid organizationId) + public async Task> GetOrganizationCiphers(Guid organizationId, bool includeMemberItems = false) { if (!await CanAccessAllCiphersAsync(organizationId)) { throw new NotFoundException(); } - var allOrganizationCiphers = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) + + bool excludeDefaultUserCollections = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation) && !includeMemberItems; + var allOrganizationCiphers = excludeDefaultUserCollections ? await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId) : From b83f95f78ccb475006e2118909a6d8cf30fb605c Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:14:02 +1000 Subject: [PATCH 276/326] [PM-25097] Remove DeleteClaimedUserAccountRefactor flag (#6364) * Remove feature flag * Remove old code --- .../OrganizationUsersController.cs | 81 +-- .../CommandResult.cs | 2 +- ...eClaimedOrganizationUserAccountCommand.cs} | 12 +- ...ClaimedOrganizationUserAccountValidator.cs | 8 +- .../DeleteUserValidationRequest.cs | 2 +- .../Errors.cs | 2 +- ...eClaimedOrganizationUserAccountCommand.cs} | 4 +- ...laimedOrganizationUserAccountValidator.cs} | 4 +- .../ValidationResult.cs | 2 +- ...teClaimedOrganizationUserAccountCommand.cs | 239 -------- ...teClaimedOrganizationUserAccountCommand.cs | 19 - src/Core/Constants.cs | 1 - ...OrganizationServiceCollectionExtensions.cs | 8 +- .../OrganizationUserControllerTests.cs | 6 +- .../OrganizationUsersControllerTests.cs | 26 +- ...medOrganizationUserAccountCommandTests.cs} | 40 +- ...dOrganizationUserAccountValidatorTests.cs} | 34 +- ...imedOrganizationUserAccountCommandTests.cs | 526 ------------------ 18 files changed, 86 insertions(+), 930 deletions(-) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext => DeleteClaimedAccount}/CommandResult.cs (98%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs => DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs} (92%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext => DeleteClaimedAccount}/DeleteClaimedOrganizationUserAccountValidator.cs (93%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext => DeleteClaimedAccount}/DeleteUserValidationRequest.cs (92%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext => DeleteClaimedAccount}/Errors.cs (97%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs => DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs} (87%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs => DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs} (65%) rename src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/{DeleteClaimedAccountvNext => DeleteClaimedAccount}/ValidationResult.cs (97%) delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteClaimedOrganizationUserAccountCommand.cs rename test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/{DeleteClaimedOrganizationUserAccountCommandvNextTests.cs => DeleteClaimedOrganizationUserAccountCommandTests.cs} (93%) rename test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/{DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs => DeleteClaimedOrganizationUserAccountValidatorTests.cs} (97%) delete mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommandTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 16d6984334..74ac9b1255 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -11,7 +11,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; @@ -61,7 +61,6 @@ public class OrganizationUsersController : Controller private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand; - private readonly IDeleteClaimedOrganizationUserAccountCommandvNext _deleteClaimedOrganizationUserAccountCommandvNext; private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly IFeatureService _featureService; @@ -90,7 +89,6 @@ public class OrganizationUsersController : Controller IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand, - IDeleteClaimedOrganizationUserAccountCommandvNext deleteClaimedOrganizationUserAccountCommandvNext, IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, IPolicyRequirementQuery policyRequirementQuery, IFeatureService featureService, @@ -119,7 +117,6 @@ public class OrganizationUsersController : Controller _organizationUserUserDetailsQuery = organizationUserUserDetailsQuery; _removeOrganizationUserCommand = removeOrganizationUserCommand; _deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand; - _deleteClaimedOrganizationUserAccountCommandvNext = deleteClaimedOrganizationUserAccountCommandvNext; _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; _policyRequirementQuery = policyRequirementQuery; _featureService = featureService; @@ -539,21 +536,22 @@ public class OrganizationUsersController : Controller [HttpDelete("{id}/delete-account")] [Authorize] - public async Task DeleteAccount(Guid orgId, Guid id) + public async Task DeleteAccount(Guid orgId, Guid id) { - if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)) + var currentUserId = _userService.GetProperUserId(User); + if (currentUserId == null) { - await DeleteAccountvNext(orgId, id); - return; + return TypedResults.Unauthorized(); } - var currentUser = await _userService.GetUserByPrincipalAsync(User); - if (currentUser == null) - { - throw new UnauthorizedAccessException(); - } + var commandResult = await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUserId.Value); - await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); + return commandResult.Result.Match( + error => error is NotFoundError + ? TypedResults.NotFound(new ErrorResponseModel(error.Message)) + : TypedResults.BadRequest(new ErrorResponseModel(error.Message)), + TypedResults.Ok + ); } [HttpPost("{id}/delete-account")] @@ -564,43 +562,24 @@ public class OrganizationUsersController : Controller await DeleteAccount(orgId, id); } - private async Task DeleteAccountvNext(Guid orgId, Guid id) - { - var currentUserId = _userService.GetProperUserId(User); - if (currentUserId == null) - { - return TypedResults.Unauthorized(); - } - - var commandResult = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteUserAsync(orgId, id, currentUserId.Value); - - return commandResult.Result.Match( - error => error is NotFoundError - ? TypedResults.NotFound(new ErrorResponseModel(error.Message)) - : TypedResults.BadRequest(new ErrorResponseModel(error.Message)), - TypedResults.Ok - ); - } - [HttpDelete("delete-account")] [Authorize] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) { - if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor)) - { - return await BulkDeleteAccountvNext(orgId, model); - } - - var currentUser = await _userService.GetUserByPrincipalAsync(User); - if (currentUser == null) + var currentUserId = _userService.GetProperUserId(User); + if (currentUserId == null) { throw new UnauthorizedAccessException(); } - var results = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id); + var result = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value); - return new ListResponseModel(results.Select(r => - new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); + var responses = result.Select(r => r.Result.Match( + error => new OrganizationUserBulkResponseModel(r.Id, error.Message), + _ => new OrganizationUserBulkResponseModel(r.Id, string.Empty) + )); + + return new ListResponseModel(responses); } [HttpPost("delete-account")] @@ -611,24 +590,6 @@ public class OrganizationUsersController : Controller return await BulkDeleteAccount(orgId, model); } - private async Task> BulkDeleteAccountvNext(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) - { - var currentUserId = _userService.GetProperUserId(User); - if (currentUserId == null) - { - throw new UnauthorizedAccessException(); - } - - var result = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value); - - var responses = result.Select(r => r.Result.Match( - error => new OrganizationUserBulkResponseModel(r.Id, error.Message), - _ => new OrganizationUserBulkResponseModel(r.Id, string.Empty) - )); - - return new ListResponseModel(responses); - } - [HttpPut("{id}/revoke")] [Authorize] public async Task RevokeAsync(Guid orgId, Guid id) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs similarity index 98% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs index 3dfbe4dbda..fbb00a908a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/CommandResult.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/CommandResult.cs @@ -1,7 +1,7 @@ using OneOf; using OneOf.Types; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; /// /// Represents the result of a command. diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs similarity index 92% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs index 3064a426fa..87c24c3ab4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNext.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountCommand.cs @@ -8,18 +8,18 @@ using Bit.Core.Services; using Microsoft.Extensions.Logging; using OneOf.Types; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; -public class DeleteClaimedOrganizationUserAccountCommandvNext( +public class DeleteClaimedOrganizationUserAccountCommand( IUserService userService, IEventService eventService, IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, IOrganizationUserRepository organizationUserRepository, IUserRepository userRepository, IPushNotificationService pushService, - ILogger logger, - IDeleteClaimedOrganizationUserAccountValidatorvNext deleteClaimedOrganizationUserAccountValidatorvNext) - : IDeleteClaimedOrganizationUserAccountCommandvNext + ILogger logger, + IDeleteClaimedOrganizationUserAccountValidator deleteClaimedOrganizationUserAccountValidator) + : IDeleteClaimedOrganizationUserAccountCommand { public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId) { @@ -35,7 +35,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNext( var claimedStatuses = await getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, orgUserIds); var internalRequests = CreateInternalRequests(organizationId, deletingUserId, orgUserIds, orgUsers, users, claimedStatuses); - var validationResults = (await deleteClaimedOrganizationUserAccountValidatorvNext.ValidateAsync(internalRequests)).ToList(); + var validationResults = (await deleteClaimedOrganizationUserAccountValidator.ValidateAsync(internalRequests)).ToList(); var validRequests = validationResults.ValidRequests(); await CancelPremiumsAsync(validRequests); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs similarity index 93% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs index 7a88841d2f..315d45ea69 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteClaimedOrganizationUserAccountValidator.cs @@ -2,14 +2,14 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Repositories; -using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext.ValidationResultHelpers; +using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount.ValidationResultHelpers; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; -public class DeleteClaimedOrganizationUserAccountValidatorvNext( +public class DeleteClaimedOrganizationUserAccountValidator( ICurrentContext currentContext, IOrganizationUserRepository organizationUserRepository, - IProviderUserRepository providerUserRepository) : IDeleteClaimedOrganizationUserAccountValidatorvNext + IProviderUserRepository providerUserRepository) : IDeleteClaimedOrganizationUserAccountValidator { public async Task>> ValidateAsync(IEnumerable requests) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteUserValidationRequest.cs similarity index 92% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteUserValidationRequest.cs index 5fd95dc73c..067d7ce04c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteUserValidationRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/DeleteUserValidationRequest.cs @@ -1,6 +1,6 @@ using Bit.Core.Entities; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; public class DeleteUserValidationRequest { diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs similarity index 97% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs index d991a882b8..6c8f7ee00c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/Errors.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; /// /// A strongly typed error containing a reason that an action failed. diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs similarity index 87% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs index 2c462a2acf..983a3a4f21 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountCommandvNext.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountCommand.cs @@ -1,6 +1,6 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; -public interface IDeleteClaimedOrganizationUserAccountCommandvNext +public interface IDeleteClaimedOrganizationUserAccountCommand { /// /// Removes a user from an organization and deletes all of their associated user data. diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs similarity index 65% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs index f6125a0355..f1a2c71b1b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/IDeleteClaimedOrganizationUserAccountValidatorvNext.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/IDeleteClaimedOrganizationUserAccountValidator.cs @@ -1,6 +1,6 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; -public interface IDeleteClaimedOrganizationUserAccountValidatorvNext +public interface IDeleteClaimedOrganizationUserAccountValidator { Task>> ValidateAsync(IEnumerable requests); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs similarity index 97% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs index 23d2fbb7ce..c84a0aeda1 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/ValidationResult.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccount/ValidationResult.cs @@ -1,7 +1,7 @@ using OneOf; using OneOf.Types; -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; /// /// Represents the result of validating a request. diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs deleted file mode 100644 index 60a1c8bfbf..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommand.cs +++ /dev/null @@ -1,239 +0,0 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Platform.Push; -using Bit.Core.Repositories; -using Bit.Core.Services; - -#nullable enable - -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; - -public class DeleteClaimedOrganizationUserAccountCommand : IDeleteClaimedOrganizationUserAccountCommand -{ - private readonly IUserService _userService; - private readonly IEventService _eventService; - private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IUserRepository _userRepository; - private readonly ICurrentContext _currentContext; - private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; - private readonly IPushNotificationService _pushService; - private readonly IOrganizationRepository _organizationRepository; - private readonly IProviderUserRepository _providerUserRepository; - public DeleteClaimedOrganizationUserAccountCommand( - IUserService userService, - IEventService eventService, - IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery, - IOrganizationUserRepository organizationUserRepository, - IUserRepository userRepository, - ICurrentContext currentContext, - IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IPushNotificationService pushService, - IOrganizationRepository organizationRepository, - IProviderUserRepository providerUserRepository) - { - _userService = userService; - _eventService = eventService; - _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; - _organizationUserRepository = organizationUserRepository; - _userRepository = userRepository; - _currentContext = currentContext; - _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; - _pushService = pushService; - _organizationRepository = organizationRepository; - _providerUserRepository = providerUserRepository; - } - - public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId) - { - var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (organizationUser == null || organizationUser.OrganizationId != organizationId) - { - throw new NotFoundException("Member not found."); - } - - var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, new[] { organizationUserId }); - var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true); - - await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, claimedStatus, hasOtherConfirmedOwners); - - var user = await _userRepository.GetByIdAsync(organizationUser.UserId!.Value); - if (user == null) - { - throw new NotFoundException("Member not found."); - } - - await _userService.DeleteAsync(user); - await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted); - } - - public async Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId) - { - var orgUsers = await _organizationUserRepository.GetManyAsync(orgUserIds); - var userIds = orgUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId!.Value).ToList(); - var users = await _userRepository.GetManyAsync(userIds); - - var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, orgUserIds); - var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true); - - var results = new List<(Guid OrganizationUserId, string? ErrorMessage)>(); - foreach (var orgUserId in orgUserIds) - { - try - { - var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId); - if (orgUser == null || orgUser.OrganizationId != organizationId) - { - throw new NotFoundException("Member not found."); - } - - await ValidateDeleteUserAsync(organizationId, orgUser, deletingUserId, claimedStatus, hasOtherConfirmedOwners); - - var user = users.FirstOrDefault(u => u.Id == orgUser.UserId); - if (user == null) - { - throw new NotFoundException("Member not found."); - } - - await ValidateUserMembershipAndPremiumAsync(user); - - results.Add((orgUserId, string.Empty)); - } - catch (Exception ex) - { - results.Add((orgUserId, ex.Message)); - } - } - - var orgUserResultsToDelete = results.Where(result => string.IsNullOrEmpty(result.ErrorMessage)); - var orgUsersToDelete = orgUsers.Where(orgUser => orgUserResultsToDelete.Any(result => orgUser.Id == result.OrganizationUserId)); - var usersToDelete = users.Where(user => orgUsersToDelete.Any(orgUser => orgUser.UserId == user.Id)); - - if (usersToDelete.Any()) - { - await DeleteManyAsync(usersToDelete); - } - - await LogDeletedOrganizationUsersAsync(orgUsers, results); - - return results; - } - - private async Task ValidateDeleteUserAsync(Guid organizationId, OrganizationUser orgUser, Guid? deletingUserId, IDictionary claimedStatus, bool hasOtherConfirmedOwners) - { - if (!orgUser.UserId.HasValue || orgUser.Status == OrganizationUserStatusType.Invited) - { - throw new BadRequestException("You cannot delete a member with Invited status."); - } - - if (deletingUserId.HasValue && orgUser.UserId.Value == deletingUserId.Value) - { - throw new BadRequestException("You cannot delete yourself."); - } - - if (orgUser.Type == OrganizationUserType.Owner) - { - if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(organizationId)) - { - throw new BadRequestException("Only owners can delete other owners."); - } - - if (!hasOtherConfirmedOwners) - { - throw new BadRequestException("Organization must have at least one confirmed owner."); - } - } - - if (orgUser.Type == OrganizationUserType.Admin && await _currentContext.OrganizationCustom(organizationId)) - { - throw new BadRequestException("Custom users can not delete admins."); - } - - if (!claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) || !isClaimed) - { - throw new BadRequestException("Member is not claimed by the organization."); - } - } - - private async Task LogDeletedOrganizationUsersAsync( - IEnumerable orgUsers, - IEnumerable<(Guid OrgUserId, string? ErrorMessage)> results) - { - var eventDate = DateTime.UtcNow; - var events = new List<(OrganizationUser OrgUser, EventType Event, DateTime? EventDate)>(); - - foreach (var (orgUserId, errorMessage) in results) - { - var orgUser = orgUsers.FirstOrDefault(ou => ou.Id == orgUserId); - // If the user was not found or there was an error, we skip logging the event - if (orgUser == null || !string.IsNullOrEmpty(errorMessage)) - { - continue; - } - - events.Add((orgUser, EventType.OrganizationUser_Deleted, eventDate)); - } - - if (events.Any()) - { - await _eventService.LogOrganizationUserEventsAsync(events); - } - } - private async Task DeleteManyAsync(IEnumerable users) - { - - await _userRepository.DeleteManyAsync(users); - foreach (var user in users) - { - await _pushService.PushLogOutAsync(user.Id); - } - - } - - private async Task ValidateUserMembershipAndPremiumAsync(User user) - { - // Check if user is the only owner of any organizations. - var onlyOwnerCount = await _organizationUserRepository.GetCountByOnlyOwnerAsync(user.Id); - if (onlyOwnerCount > 0) - { - throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); - } - - var orgs = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed); - if (orgs.Count == 1) - { - var org = await _organizationRepository.GetByIdAsync(orgs.First().OrganizationId); - if (org != null && (!org.Enabled || string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))) - { - var orgCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(org.Id); - if (orgCount <= 1) - { - await _organizationRepository.DeleteAsync(org); - } - else - { - throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one organization. Please delete these organizations or upgrade another user."); - } - } - } - - var onlyOwnerProviderCount = await _providerUserRepository.GetCountByOnlyOwnerAsync(user.Id); - if (onlyOwnerProviderCount > 0) - { - throw new BadRequestException("Cannot delete this user because it is the sole owner of at least one provider. Please delete these providers or upgrade another user."); - } - - if (!string.IsNullOrWhiteSpace(user.GatewaySubscriptionId)) - { - try - { - await _userService.CancelPremiumAsync(user); - } - catch (GatewayException) { } - } - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteClaimedOrganizationUserAccountCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteClaimedOrganizationUserAccountCommand.cs deleted file mode 100644 index 1c79687be9..0000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IDeleteClaimedOrganizationUserAccountCommand.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable enable - -namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; - -public interface IDeleteClaimedOrganizationUserAccountCommand -{ - /// - /// Removes a user from an organization and deletes all of their associated user data. - /// - Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId); - - /// - /// Removes multiple users from an organization and deletes all of their associated user data. - /// - /// - /// An error message for each user that could not be removed, otherwise null. - /// - Task> DeleteManyUsersAsync(Guid organizationId, IEnumerable orgUserIds, Guid? deletingUserId); -} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5c00db9da4..97c1399719 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -134,7 +134,6 @@ public static class FeatureFlagKeys public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string CipherRepositoryBulkResourceCreation = "pm-24951-cipher-repository-bulk-resource-creation-service"; public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; - public const string DeleteClaimedUserAccountRefactor = "pm-25094-refactor-delete-managed-organization-user-command"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 1c38a27d1e..da05bc929c 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -13,7 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; @@ -132,12 +132,10 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); - // vNext implementations (feature flagged) - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services) diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs index b7839467e8..7c61a88bd8 100644 --- a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerTests.cs @@ -7,7 +7,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Entities; @@ -33,10 +33,6 @@ public class OrganizationUserControllerTests : IClassFixture sutProvider) - { - sutProvider.GetDependency().ManageUsers(orgId).Returns(true); - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser); - - await sutProvider.Sut.DeleteAccount(orgId, id); - - await sutProvider.GetDependency() - .Received(1) - .DeleteUserAsync(orgId, id, currentUser.Id); - } - - [Theory] - [BitAutoData] - public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException( + public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult( Guid orgId, Guid id, SutProvider sutProvider) { - sutProvider.GetDependency().ManageUsers(orgId).Returns(true); - sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null); - await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteAccount(orgId, id)); + var result = await sutProvider.Sut.DeleteAccount(orgId, id); + + Assert.IsType(result); } [Theory] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs similarity index 93% rename from test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs rename to test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs index 679c1914c6..c223520a04 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandvNextTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountCommandTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; @@ -17,12 +17,12 @@ using Xunit; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; [SutProviderCustomize] -public class DeleteClaimedOrganizationUserAccountCommandvNextTests +public class DeleteClaimedOrganizationUserAccountCommandTests { [Theory] [BitAutoData] public async Task DeleteUserAsync_WithValidSingleUser_CallsDeleteManyUsersAsync( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -65,7 +65,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task DeleteManyUsersAsync_WithEmptyUserIds_ReturnsEmptyResults( - SutProvider sutProvider, + SutProvider sutProvider, Guid organizationId, Guid deletingUserId) { @@ -77,7 +77,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents( - SutProvider sutProvider, + SutProvider sutProvider, User user1, User user2, Guid organizationId, @@ -135,7 +135,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task DeleteManyUsersAsync_WithValidationErrors_ReturnsErrorResults( - SutProvider sutProvider, + SutProvider sutProvider, Guid organizationId, Guid orgUserId1, Guid orgUserId2, @@ -183,7 +183,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task DeleteManyUsersAsync_WithMixedValidationResults_HandlesPartialSuccessCorrectly( - SutProvider sutProvider, + SutProvider sutProvider, User validUser, Guid organizationId, Guid validOrgUserId, @@ -243,7 +243,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task DeleteManyUsersAsync_CancelPremiumsAsync_HandlesGatewayExceptionAndLogsWarning( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -285,7 +285,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests await sutProvider.GetDependency().Received(1).CancelPremiumAsync(user); await AssertSuccessfulUserOperations(sutProvider, [user], [orgUser]); - sutProvider.GetDependency>() + sutProvider.GetDependency>() .Received(1) .Log( LogLevel.Warning, @@ -299,7 +299,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task CreateInternalRequests_CreatesCorrectRequestsForAllUsers( - SutProvider sutProvider, + SutProvider sutProvider, User user1, User user2, Guid organizationId, @@ -326,7 +326,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any>()) .Returns(claimedStatuses); - sutProvider.GetDependency() + sutProvider.GetDependency() .ValidateAsync(Arg.Any>()) .Returns(callInfo => { @@ -338,7 +338,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests await sutProvider.Sut.DeleteManyUsersAsync(organizationId, orgUserIds, deletingUserId); // Assert - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) .ValidateAsync(Arg.Is>(requests => requests.Count() == 2 && @@ -359,7 +359,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests [Theory] [BitAutoData] public async Task GetUsersAsync_WithNullUserIds_ReturnsEmptyCollection( - SutProvider sutProvider, + SutProvider sutProvider, Guid organizationId, Guid deletingUserId, [OrganizationUser] OrganizationUser orgUserWithoutUserId) @@ -374,7 +374,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests .GetManyAsync(Arg.Is>(ids => !ids.Any())) .Returns([]); - sutProvider.GetDependency() + sutProvider.GetDependency() .ValidateAsync(Arg.Any>()) .Returns(callInfo => { @@ -386,7 +386,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests await sutProvider.Sut.DeleteManyUsersAsync(organizationId, [orgUserWithoutUserId.Id], deletingUserId); // Assert - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) .ValidateAsync(Arg.Is>(requests => requests.Count() == 1 && @@ -406,7 +406,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests ValidationResultHelpers.Invalid(request, error); private static void SetupRepositoryMocks( - SutProvider sutProvider, + SutProvider sutProvider, ICollection orgUsers, IEnumerable users, Guid organizationId, @@ -426,16 +426,16 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests } private static void SetupValidatorMock( - SutProvider sutProvider, + SutProvider sutProvider, IEnumerable> validationResults) { - sutProvider.GetDependency() + sutProvider.GetDependency() .ValidateAsync(Arg.Any>()) .Returns(validationResults); } private static async Task AssertSuccessfulUserOperations( - SutProvider sutProvider, + SutProvider sutProvider, IEnumerable expectedUsers, IEnumerable expectedOrgUsers) { @@ -457,7 +457,7 @@ public class DeleteClaimedOrganizationUserAccountCommandvNextTests events.Any(e => e.Item1.Id == expectedOrgUser.Id && e.Item2 == EventType.OrganizationUser_Deleted)))); } - private static async Task AssertNoUserOperations(SutProvider sutProvider) + private static async Task AssertNoUserOperations(SutProvider sutProvider) { await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteManyAsync(default); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().PushLogOutAsync(default); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorTests.cs similarity index 97% rename from test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs rename to test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorTests.cs index e51df6a626..30cc574ead 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorvNextTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedAccountvNext/DeleteClaimedOrganizationUserAccountValidatorTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -13,12 +13,12 @@ using Xunit; namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext; [SutProviderCustomize] -public class DeleteClaimedOrganizationUserAccountValidatorvNextTests +public class DeleteClaimedOrganizationUserAccountValidatorTests { [Theory] [BitAutoData] public async Task ValidateAsync_WithValidSingleRequest_ReturnsValidResult( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -50,7 +50,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithMultipleValidRequests_ReturnsAllValidResults( - SutProvider sutProvider, + SutProvider sutProvider, User user1, User user2, Guid organizationId, @@ -97,7 +97,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithNullUser_ReturnsUserNotFoundError( - SutProvider sutProvider, + SutProvider sutProvider, Guid organizationId, Guid deletingUserId, [OrganizationUser] OrganizationUser organizationUser) @@ -123,7 +123,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId) @@ -149,7 +149,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithInvitedUser_ReturnsInvalidUserStatusError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -178,7 +178,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WhenDeletingYourself_ReturnsCannotDeleteYourselfError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, [OrganizationUser] OrganizationUser organizationUser) @@ -206,7 +206,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithUnclaimedUser_ReturnsUserNotClaimedError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -235,7 +235,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsNotOwner_ReturnsCannotDeleteOwnersError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -266,7 +266,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_DeletingOwnerWhenCurrentUserIsOwner_ReturnsValidResult( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -296,7 +296,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithSoleOwnerOfOrganization_ReturnsSoleOwnerError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -331,7 +331,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithSoleProviderOwner_ReturnsSoleProviderError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -366,7 +366,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_CustomUserDeletingAdmin_ReturnsCannotDeleteAdminsError( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -397,7 +397,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_AdminDeletingAdmin_ReturnsValidResult( - SutProvider sutProvider, + SutProvider sutProvider, User user, Guid organizationId, Guid deletingUserId, @@ -427,7 +427,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests [Theory] [BitAutoData] public async Task ValidateAsync_WithMixedValidAndInvalidRequests_ReturnsCorrespondingResults( - SutProvider sutProvider, + SutProvider sutProvider, User validUser, User invalidUser, Guid organizationId, @@ -475,7 +475,7 @@ public class DeleteClaimedOrganizationUserAccountValidatorvNextTests } private static void SetupMocks( - SutProvider sutProvider, + SutProvider sutProvider, Guid organizationId, Guid userId, OrganizationUserType currentUserType = OrganizationUserType.Owner) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommandTests.cs deleted file mode 100644 index 7f1b101d7a..0000000000 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/DeleteClaimedOrganizationUserAccountCommandTests.cs +++ /dev/null @@ -1,526 +0,0 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers; - -[SutProviderCustomize] -public class DeleteClaimedOrganizationUserAccountCommandTests -{ - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_WithValidUser_DeletesUserAndLogsEvent( - SutProvider sutProvider, User user, Guid deletingUserId, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser) - { - // Arrange - organizationUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync( - organizationUser.OrganizationId, - Arg.Is>(ids => ids.Contains(organizationUser.Id))) - .Returns(new Dictionary { { organizationUser.Id, true } }); - - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync( - organizationUser.OrganizationId, - Arg.Is>(ids => ids.Contains(organizationUser.Id)), - includeProvider: Arg.Any()) - .Returns(true); - - // Act - await sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId); - - // Assert - await sutProvider.GetDependency().Received(1).DeleteAsync(user); - await sutProvider.GetDependency().Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Deleted); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_WithUserNotFound_ThrowsException( - SutProvider sutProvider, - Guid organizationId, Guid organizationUserId) - { - // Arrange - sutProvider.GetDependency() - .GetByIdAsync(organizationUserId) - .Returns((OrganizationUser?)null); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null)); - - // Assert - Assert.Equal("Member not found.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_DeletingYourself_ThrowsException( - SutProvider sutProvider, - User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser, - Guid deletingUserId) - { - // Arrange - organizationUser.UserId = user.Id = deletingUserId; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency().GetByIdAsync(user.Id) - .Returns(user); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId)); - - // Assert - Assert.Equal("You cannot delete yourself.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_WhenUserIsInvited_ThrowsException( - SutProvider sutProvider, - [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser organizationUser) - { - // Arrange - organizationUser.UserId = null; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null)); - - // Assert - Assert.Equal("You cannot delete a member with Invited status.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_WhenCustomUserDeletesAdmin_ThrowsException( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Admin)] OrganizationUser organizationUser, - Guid deletingUserId) - { - // Arrange - organizationUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency().GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .OrganizationCustom(organizationUser.OrganizationId) - .Returns(true); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId)); - - // Assert - Assert.Equal("Custom users can not delete admins.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_DeletingOwnerWhenNotOwner_ThrowsException( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser, - Guid deletingUserId) - { - // Arrange - organizationUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency().GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .OrganizationOwner(organizationUser.OrganizationId) - .Returns(false); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId)); - - // Assert - Assert.Equal("Only owners can delete other owners.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_DeletingLastConfirmedOwner_ThrowsException( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser organizationUser, - Guid deletingUserId) - { - // Arrange - organizationUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency().GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .OrganizationOwner(organizationUser.OrganizationId) - .Returns(true); - - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync( - organizationUser.OrganizationId, - Arg.Is>(ids => ids.Contains(organizationUser.Id)), - includeProvider: Arg.Any()) - .Returns(false); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, deletingUserId)); - - // Assert - Assert.Equal("Organization must have at least one confirmed owner.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteUserAsync_WithUserNotManaged_ThrowsException( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser organizationUser) - { - // Arrange - organizationUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - sutProvider.GetDependency().GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync(organizationUser.OrganizationId, Arg.Any>()) - .Returns(new Dictionary { { organizationUser.Id, false } }); - - // Act - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.DeleteUserAsync(organizationUser.OrganizationId, organizationUser.Id, null)); - - // Assert - Assert.Equal("Member is not claimed by the organization.", exception.Message); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WithValidUsers_DeletesUsersAndLogsEvents( - SutProvider sutProvider, User user1, User user2, Guid organizationId, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2) - { - // Arrange - orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId; - orgUser1.UserId = user1.Id; - orgUser2.UserId = user2.Id; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser1, orgUser2 }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(ids => ids.Contains(user1.Id) && ids.Contains(user2.Id))) - .Returns(new[] { user1, user2 }); - - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any>()) - .Returns(new Dictionary { { orgUser1.Id, true }, { orgUser2.Id, true } }); - - // Act - var userIds = new[] { orgUser1.Id, orgUser2.Id }; - var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, userIds, null); - - // Assert - Assert.Equal(2, results.Count()); - Assert.All(results, r => Assert.Empty(r.Item2)); - - await sutProvider.GetDependency().Received(1).GetManyAsync(userIds); - await sutProvider.GetDependency().Received(1).DeleteManyAsync(Arg.Is>(users => users.Any(u => u.Id == user1.Id) && users.Any(u => u.Id == user2.Id))); - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync( - Arg.Is>(events => - events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1 - && events.Count(e => e.Item1.Id == orgUser2.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1)); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenUserNotFound_ReturnsErrorMessage( - SutProvider sutProvider, - Guid organizationId, - Guid orgUserId) - { - // Act - var result = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUserId }, null); - - // Assert - Assert.Single(result); - Assert.Equal(orgUserId, result.First().Item1); - Assert.Contains("Member not found.", result.First().Item2); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .DeleteManyAsync(default); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenDeletingYourself_ReturnsErrorMessage( - SutProvider sutProvider, - User user, [OrganizationUser] OrganizationUser orgUser, Guid deletingUserId) - { - // Arrange - orgUser.UserId = user.Id = deletingUserId; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(ids => ids.Contains(user.Id))) - .Returns(new[] { user }); - - // Act - var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId); - - // Assert - Assert.Single(result); - Assert.Equal(orgUser.Id, result.First().Item1); - Assert.Contains("You cannot delete yourself.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenUserIsInvited_ReturnsErrorMessage( - SutProvider sutProvider, - [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser) - { - // Arrange - orgUser.UserId = null; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser }); - - // Act - var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null); - - // Assert - Assert.Single(result); - Assert.Equal(orgUser.Id, result.First().Item1); - Assert.Contains("You cannot delete a member with Invited status.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenDeletingOwnerAsNonOwner_ReturnsErrorMessage( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - Guid deletingUserId) - { - // Arrange - orgUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new[] { user }); - - sutProvider.GetDependency() - .OrganizationOwner(orgUser.OrganizationId) - .Returns(false); - - var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId); - - Assert.Single(result); - Assert.Equal(orgUser.Id, result.First().Item1); - Assert.Contains("Only owners can delete other owners.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenDeletingLastOwner_ReturnsErrorMessage( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser, - Guid deletingUserId) - { - // Arrange - orgUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new[] { user }); - - sutProvider.GetDependency() - .OrganizationOwner(orgUser.OrganizationId) - .Returns(true); - - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any>(), Arg.Any()) - .Returns(false); - - // Act - var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, deletingUserId); - - // Assert - Assert.Single(result); - Assert.Equal(orgUser.Id, result.First().Item1); - Assert.Contains("Organization must have at least one confirmed owner.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_WhenUserNotManaged_ReturnsErrorMessage( - SutProvider sutProvider, User user, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser) - { - // Arrange - orgUser.UserId = user.Id; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(ids => ids.Contains(orgUser.UserId.Value))) - .Returns(new[] { user }); - - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync(Arg.Any(), Arg.Any>()) - .Returns(new Dictionary { { orgUser.Id, false } }); - - // Act - var result = await sutProvider.Sut.DeleteManyUsersAsync(orgUser.OrganizationId, new[] { orgUser.Id }, null); - - // Assert - Assert.Single(result); - Assert.Equal(orgUser.Id, result.First().Item1); - Assert.Contains("Member is not claimed by the organization.", result.First().Item2); - await sutProvider.GetDependency().Received(0).DeleteAsync(Arg.Any()); - await sutProvider.GetDependency().Received(0) - .LogOrganizationUserEventsAsync(Arg.Any>()); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyUsersAsync_MixedValidAndInvalidUsers_ReturnsAppropriateResults( - SutProvider sutProvider, User user1, User user3, - Guid organizationId, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1, - [OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.User)] OrganizationUser orgUser2, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser3) - { - // Arrange - orgUser1.UserId = user1.Id; - orgUser2.UserId = null; - orgUser3.UserId = user3.Id; - orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organizationId; - - sutProvider.GetDependency() - .GetManyAsync(Arg.Any>()) - .Returns(new List { orgUser1, orgUser2, orgUser3 }); - - sutProvider.GetDependency() - .GetManyAsync(Arg.Is>(ids => ids.Contains(user1.Id) && ids.Contains(user3.Id))) - .Returns(new[] { user1, user3 }); - - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync(organizationId, Arg.Any>()) - .Returns(new Dictionary { { orgUser1.Id, true }, { orgUser3.Id, false } }); - - // Act - var results = await sutProvider.Sut.DeleteManyUsersAsync(organizationId, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, null); - - // Assert - Assert.Equal(3, results.Count()); - Assert.Empty(results.First(r => r.Item1 == orgUser1.Id).Item2); - Assert.Equal("You cannot delete a member with Invited status.", results.First(r => r.Item1 == orgUser2.Id).Item2); - Assert.Equal("Member is not claimed by the organization.", results.First(r => r.Item1 == orgUser3.Id).Item2); - - await sutProvider.GetDependency().Received(1).LogOrganizationUserEventsAsync( - Arg.Is>(events => - events.Count(e => e.Item1.Id == orgUser1.Id && e.Item2 == EventType.OrganizationUser_Deleted) == 1)); - } -} From 179684a9e64b22a9aa2c0f77c5021e179824ea6e Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Thu, 25 Sep 2025 07:46:59 +0200 Subject: [PATCH 277/326] Begin pilot program for Claude code reviews with initial system prompt (#6371) * Rough draft of a markdown file to give context to Claude. --- CLAUDE.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..db0252ad8c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# Bitwarden Server - Claude Code Configuration + +## Critical Rules + +- **NEVER** edit: `/bin/`, `/obj/`, `/.git/`, `/.vs/`, `/packages/`, generated migration files +- **Security First**: All code changes must prioritize cryptographic integrity and data protection +- **Test Coverage**: New features require xUnit unit tests with NSubstitute mocking +- **Check CODEOWNERS requirements**: The repo has a `.github/CODEOWNERS` file to define team ownership for different parts of the codebase. Respect that code owners have final authority over their designated areas + +## Project Context + +**Architecture**: CQRS pattern with feature-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 + +## Development Standards + +### CQRS Pattern + +- Commands: `/src/Core/[Feature]/Commands/` +- Queries: `/src/Core/[Feature]/Queries/` +- Handlers implement `ICommandHandler` or `IQueryHandler` + +### API Conventions + +- RESTful endpoints with standard HTTP status codes +- Consistent error response: `{ "error": { "message": "..." } }` +- Pagination: `?skip=0&take=25` +- API versioning: `/api/v1/` + +### Database Migrations + +- **SQL Server**: Manual scripts in `/util/Migrator/DbScripts/` +- **Other DBs**: EF Core migrations via `pwsh ef_migrate.ps1` + +## Security Requirements + +- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA +- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults +- **Validation**: Input sanitization, parameterized queries, rate limiting +- **Logging**: Structured logs, no PII/sensitive data in logs + +## Code Review Checklist + +- Security impact assessed +- xUnit tests added/updated +- Performance impact considered +- Error handling implemented +- Breaking changes documented +- CI passes: build, test, lint + +## References + +- [Architecture](https://contributing.bitwarden.com/architecture/server/) +- [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) +- [Setup Guide](https://contributing.bitwarden.com/getting-started/server/guide/) +- [Code Style](https://contributing.bitwarden.com/contributing/code-style/) +- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/) +- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions) From 222436589c503917b18f4536f529525116a78c50 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 25 Sep 2025 12:37:29 -0400 Subject: [PATCH 278/326] Enhance Claude instructions (#6378) * Enhance Claude instructions * Further simplify language --- CLAUDE.md | 75 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index db0252ad8c..d07bd3f3e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,38 +2,30 @@ ## Critical Rules -- **NEVER** edit: `/bin/`, `/obj/`, `/.git/`, `/.vs/`, `/packages/`, generated migration files -- **Security First**: All code changes must prioritize cryptographic integrity and data protection -- **Test Coverage**: New features require xUnit unit tests with NSubstitute mocking -- **Check CODEOWNERS requirements**: The repo has a `.github/CODEOWNERS` file to define team ownership for different parts of the codebase. Respect that code owners have final authority over their designated areas +- **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**: CQRS pattern with feature-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 +- **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 -## Development Standards +## Project Structure -### CQRS Pattern - -- Commands: `/src/Core/[Feature]/Commands/` -- Queries: `/src/Core/[Feature]/Queries/` -- Handlers implement `ICommandHandler` or `IQueryHandler` - -### API Conventions - -- RESTful endpoints with standard HTTP status codes -- Consistent error response: `{ "error": { "message": "..." } }` -- Pagination: `?skip=0&take=25` -- API versioning: `/api/v1/` - -### Database Migrations - -- **SQL Server**: Manual scripts in `/util/Migrator/DbScripts/` -- **Other DBs**: EF Core migrations via `pwsh ef_migrate.ps1` +- **Source Code**: `/src/` - Services and core infrastructure +- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix +- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts +- **Dev Tools**: `/dev/` - Local development helpers +- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development ## Security Requirements @@ -42,20 +34,39 @@ - **Validation**: Input sanitization, parameterized queries, rate limiting - **Logging**: Structured logs, no PII/sensitive data in logs +## Common Commands + +- **Build**: `dotnet build` +- **Test**: `dotnet test` +- **Run locally**: `dotnet run --project src/Api` +- **Database update**: `pwsh dev/migrate.ps1` +- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1` + ## Code Review Checklist - Security impact assessed -- xUnit tests added/updated +- xUnit tests added / updated - Performance impact considered - Error handling implemented - Breaking changes documented - CI passes: build, test, lint +- Feature flags considered for new features +- CODEOWNERS file respected + +### Key Architectural Decisions + +- Use .NET nullable reference types (ADR 0024) +- TryAdd dependency injection pattern (ADR 0026) +- Authorization patterns (ADR 0022) +- OpenTelemetry for observability (ADR 0020) +- Log to standard output (ADR 0021) ## References -- [Architecture](https://contributing.bitwarden.com/architecture/server/) -- [Contributing Guidelines](https://contributing.bitwarden.com/contributing/) -- [Setup Guide](https://contributing.bitwarden.com/getting-started/server/guide/) -- [Code Style](https://contributing.bitwarden.com/contributing/code-style/) +- [Server architecture](https://contributing.bitwarden.com/architecture/server/) +- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/) +- [Contributing guidelines](https://contributing.bitwarden.com/contributing/) +- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/) +- [Code style](https://contributing.bitwarden.com/contributing/code-style/) - [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/) - [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions) From 6466c00acde2d067115c6274c21e2dbad852f1d0 Mon Sep 17 00:00:00 2001 From: Dave <3836813+enmande@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:37:36 -0400 Subject: [PATCH 279/326] fix(user-decryption-options) [PM-23174]: ManageAccountRecovery Permission Forces Master Password Set (#6230) * fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Update tests, add OrganizationUser fixture customization for Permissions * fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Update hasManageResetPasswordPermission evaluation. * PM-23174 - Add TODO for endpoint per sync discussion with Dave * fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Clean up comments. * fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Remove an outdated comment. * fix(user-decryption-options): ManageAccountRecovery Permission Forces MP Set - Elaborate on comments around Organization User invite-time evaluation. * fix(user-decryption-options): Use currentContext for Provider relationships, update comments, and feature flag the change. * fix(user-decryption-options): Update test suite and provide additional comments for future flag removal. --------- Co-authored-by: Jared Snider --- src/Core/Constants.cs | 2 + .../UserDecryptionOptionsBuilder.cs | 104 ++++++++++++++---- .../AutoFixture/OrganizationUserFixtures.cs | 26 +++++ .../UserDecryptionOptionsBuilderTests.cs | 71 ++++++++++-- 4 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 97c1399719..ba8c1a84cd 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -144,6 +144,8 @@ public static class FeatureFlagKeys public const string Otp6Digits = "pm-18612-otp-6-digits"; public const string FailedTwoFactorEmail = "pm-24425-send-2fa-failed-email"; 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"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs index dc27842210..136c3f7298 100644 --- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Response; using Bit.Core.Auth.Utilities; @@ -7,6 +8,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.KeyManagement.Models.Response; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Identity.Utilities; @@ -24,6 +26,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; + private readonly IFeatureService _featureService; private UserDecryptionOptions _options = new UserDecryptionOptions(); private User _user = null!; @@ -34,13 +37,15 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder ICurrentContext currentContext, IDeviceRepository deviceRepository, IOrganizationUserRepository organizationUserRepository, - ILoginApprovingClientTypes loginApprovingClientTypes + ILoginApprovingClientTypes loginApprovingClientTypes, + IFeatureService featureService ) { _currentContext = currentContext; _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; _loginApprovingClientTypes = loginApprovingClientTypes; + _featureService = featureService; } public IUserDecryptionOptionsBuilder ForUser(User user) @@ -65,8 +70,10 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder { if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled) { - _options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey); + _options.WebAuthnPrfOption = + new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey); } + return this; } @@ -74,7 +81,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder { BuildMasterPasswordUnlock(); BuildKeyConnectorOptions(); - await BuildTrustedDeviceOptions(); + await BuildTrustedDeviceOptionsAsync(); return _options; } @@ -87,13 +94,14 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder } var ssoConfigurationData = _ssoConfig.GetData(); - if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl)) + if (ssoConfigurationData is { MemberDecryptionType: MemberDecryptionType.KeyConnector } && + !string.IsNullOrEmpty(ssoConfigurationData.KeyConnectorUrl)) { _options.KeyConnectorOption = new KeyConnectorUserDecryptionOption(ssoConfigurationData.KeyConnectorUrl); } } - private async Task BuildTrustedDeviceOptions() + private async Task BuildTrustedDeviceOptionsAsync() { // TrustedDeviceEncryption only exists for SSO, if that changes then these guards should change if (_ssoConfig == null) @@ -101,7 +109,8 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder return; } - var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption }; + var isTdeActive = _ssoConfig.GetData() is + { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption }; var isTdeOffboarding = !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive; if (!isTdeActive && !isTdeOffboarding) { @@ -120,25 +129,51 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder if (_device != null) { var allDevices = await _deviceRepository.GetManyByUserIdAsync(_user.Id); - // Checks if the current user has any devices that are capable of approving login with device requests except for - // their current device. - // NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting. - hasLoginApprovingDevice = allDevices.Any(d => d.Identifier != _device.Identifier && _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); + // Checks if the current user has any devices that are capable of approving login with device requests + // except for their current device. + hasLoginApprovingDevice = allDevices.Any(d => + d.Identifier != _device.Identifier && + _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); } - // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP - var hasManageResetPasswordPermission = false; - // when a user is being created via JIT provisioning, they will not have any orgs so we can't assume we will have orgs here - if (_currentContext.Organizations != null && _currentContext.Organizations.Any(o => o.Id == _ssoConfig.OrganizationId)) - { - // TDE requires single org so grabbing first org & id is fine. - hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId); - } - - // If sso configuration data is not null then I know for sure that ssoConfiguration isn't null + // Just-in-time-provisioned users, which can include users invited to a TDE organization with SSO and granted + // the Admin/Owner role or Custom user role with ManageResetPassword permission, will not have claims available + // in context to reflect this permission if granted as part of an invite for the current organization. + // Therefore, as written today, CurrentContext will not surface those permissions for those users. + // In order to make this check accurate at first login for all applicable cases, we have to go back to the + // database record. + // In the TDE flow, the users will have been JIT-provisioned at SSO callback time, and the relationship between + // user and organization user will have been codified. var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id); + var hasManageResetPasswordPermission = false; + if (_featureService.IsEnabled(FeatureFlagKeys.PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword)) + { + hasManageResetPasswordPermission = await EvaluateHasManageResetPasswordPermission(); + } + else + { + // TODO: PM-26065 remove use of above feature flag from the server, and remove this branching logic, which + // has been replaced by EvaluateHasManageResetPasswordPermission. + // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP. + // When removing feature flags, please also see notes and removals intended for test suite in + // Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue. + + // when a user is being created via JIT provisioning, they will not have any orgs so we can't assume we will have orgs here + if (_currentContext.Organizations != null && _currentContext.Organizations.Any(o => o.Id == _ssoConfig.OrganizationId)) + { + // TDE requires single org so grabbing first org & id is fine. + hasManageResetPasswordPermission = await _currentContext.ManageResetPassword(_ssoConfig!.OrganizationId); + } + + // If sso configuration data is not null then I know for sure that ssoConfiguration isn't null + + // NOTE: Commented from original impl because the organization user repository call has been hoisted to support + // branching paths through flagging. + //organizationUser = await _organizationUserRepository.GetByOrganizationAsync(_ssoConfig.OrganizationId, _user.Id); + + hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin); + } - hasManageResetPasswordPermission |= organizationUser != null && (organizationUser.Type == OrganizationUserType.Owner || organizationUser.Type == OrganizationUserType.Admin); // They are only able to be approved by an admin if they have enrolled is reset password var hasAdminApproval = organizationUser != null && !string.IsNullOrEmpty(organizationUser.ResetPasswordKey); @@ -149,6 +184,31 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder isTdeOffboarding, encryptedPrivateKey, encryptedUserKey); + return; + + async Task EvaluateHasManageResetPasswordPermission() + { + // PM-23174 + // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP + if (organizationUser == null) + { + return false; + } + + var organizationUserHasResetPasswordPermission = + // The repository will pull users in all statuses, so we also need to ensure that revoked-status users do not have + // permissions sent down. + organizationUser.Status is OrganizationUserStatusType.Invited or OrganizationUserStatusType.Accepted or + OrganizationUserStatusType.Confirmed && + // Admins and owners get ManageResetPassword functionally "for free" through their role. + (organizationUser.Type is OrganizationUserType.Admin or OrganizationUserType.Owner || + // Custom users can have the ManagePasswordReset permission assigned directly. + organizationUser.GetPermissions() is { ManageResetPassword: true }); + + return organizationUserHasResetPasswordPermission || + // A provider user for the given organization gets ManageResetPassword through that relationship. + await _currentContext.ProviderUserForOrgAsync(_ssoConfig.OrganizationId); + } } private void BuildMasterPasswordUnlock() diff --git a/test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs b/test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs new file mode 100644 index 0000000000..e4d40601c5 --- /dev/null +++ b/test/Identity.Test/AutoFixture/OrganizationUserFixtures.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Identity.Test.AutoFixture; + +internal class OrganizationUserWithDefaultPermissionsCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + // On OrganizationUser, Permissions can be JSON data (as string) or sometimes null. + // Entity APIs should prevent it from being anything else. + // An un-modified fixture for OrganizationUser will return a bare string Permissions{guid} + // in the member, throwing a JsonException on deserialization of a bare string. + .With(organizationUser => organizationUser.Permissions, CoreHelpers.ClassToJsonData(new Permissions()))); + } +} + +public class OrganizationUserWithDefaultPermissionsAttribute : CustomizeAttribute +{ + public override ICustomization GetCustomization(ParameterInfo parameter) => new OrganizationUserWithDefaultPermissionsCustomization(); +} diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index b44dfe8d5f..37e88b0ec0 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -1,15 +1,20 @@ -using Bit.Core.Auth.Entities; +using Bit.Core; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Identity.IdentityServer; +using Bit.Identity.Test.AutoFixture; using Bit.Identity.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; +using User = Bit.Core.Entities.User; namespace Bit.Identity.Test.IdentityServer; @@ -20,6 +25,7 @@ public class UserDecryptionOptionsBuilderTests private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private readonly UserDecryptionOptionsBuilder _builder; + private readonly IFeatureService _featureService; public UserDecryptionOptionsBuilderTests() { @@ -27,7 +33,8 @@ public class UserDecryptionOptionsBuilderTests _deviceRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); _loginApprovingClientTypes = Substitute.For(); - _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes); + _featureService = Substitute.For(); + _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes, _featureService); var user = new User(); _builder.ForUser(user); } @@ -220,19 +227,65 @@ public class UserDecryptionOptionsBuilderTests Assert.False(result.TrustedDeviceOption?.HasLoginApprovingDevice); } - [Theory, BitAutoData] + /// + /// This logic has been flagged as part of PM-23174. + /// When removing the server flag, please also remove this test, and remove the FeatureService + /// dependency from this suite and the following test. + /// + /// + /// + /// + /// + /// + /// + [Theory] + [BitAutoData(OrganizationUserType.Custom)] public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue( + OrganizationUserType organizationUserType, SsoConfig ssoConfig, SsoConfigurationData configurationData, - CurrentContextOrganization organization) + CurrentContextOrganization organization, + [OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser, + User user) { configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; ssoConfig.Data = configurationData.Serialize(); ssoConfig.OrganizationId = organization.Id; - _currentContext.Organizations.Returns(new List(new CurrentContextOrganization[] { organization })); + _currentContext.Organizations.Returns([organization]); _currentContext.ManageResetPassword(organization.Id).Returns(true); + organizationUser.Type = organizationUserType; + organizationUser.OrganizationId = organization.Id; + organizationUser.UserId = user.Id; + organizationUser.SetPermissions(new Permissions() { ManageResetPassword = true }); + _organizationUserRepository.GetByOrganizationAsync(ssoConfig.OrganizationId, user.Id).Returns(organizationUser); - var result = await _builder.WithSso(ssoConfig).BuildAsync(); + var result = await _builder.ForUser(user).WithSso(ssoConfig).BuildAsync(); + + Assert.True(result.TrustedDeviceOption?.HasManageResetPasswordPermission); + } + + [Theory] + [BitAutoData(OrganizationUserType.Custom)] + public async Task Build_WhenManageResetPasswordPermissions_ShouldFetchUserFromRepositoryAndReturnHasManageResetPasswordPermissionTrue( + OrganizationUserType organizationUserType, + SsoConfig ssoConfig, + SsoConfigurationData configurationData, + CurrentContextOrganization organization, + [OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser, + User user) + { + _featureService.IsEnabled(FeatureFlagKeys.PM23174ManageAccountRecoveryPermissionDrivesTheNeedToSetMasterPassword) + .Returns(true); + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + ssoConfig.OrganizationId = organization.Id; + organizationUser.Type = organizationUserType; + organizationUser.OrganizationId = organization.Id; + organizationUser.UserId = user.Id; + organizationUser.SetPermissions(new Permissions() { ManageResetPassword = true }); + _organizationUserRepository.GetByOrganizationAsync(ssoConfig.OrganizationId, user.Id).Returns(organizationUser); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).BuildAsync(); Assert.True(result.TrustedDeviceOption?.HasManageResetPasswordPermission); } @@ -241,7 +294,7 @@ public class UserDecryptionOptionsBuilderTests public async Task Build_WhenIsOwnerInvite_ShouldReturnHasManageResetPasswordPermissionTrue( SsoConfig ssoConfig, SsoConfigurationData configurationData, - OrganizationUser organizationUser, + [OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser, User user) { configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; @@ -258,7 +311,7 @@ public class UserDecryptionOptionsBuilderTests public async Task Build_WhenIsAdminInvite_ShouldReturnHasManageResetPasswordPermissionTrue( SsoConfig ssoConfig, SsoConfigurationData configurationData, - OrganizationUser organizationUser, + [OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser, User user) { configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; @@ -275,7 +328,7 @@ public class UserDecryptionOptionsBuilderTests public async Task Build_WhenUserHasEnrolledIntoPasswordReset_ShouldReturnHasAdminApprovalTrue( SsoConfig ssoConfig, SsoConfigurationData configurationData, - OrganizationUser organizationUser, + [OrganizationUserWithDefaultPermissions] OrganizationUser organizationUser, User user) { configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; From 0df22ff5816ac22d6fe5b669fe83c874cc5721a6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 25 Sep 2025 19:05:48 -0400 Subject: [PATCH 280/326] null coalesce collections to an empty array (#6381) --- .../Public/Models/Request/MemberCreateRequestModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs index 6813610325..b3182601b5 100644 --- a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs +++ b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs @@ -31,7 +31,7 @@ public class MemberCreateRequestModel : MemberUpdateRequestModel { Emails = new[] { Email }, Type = Type.Value, - Collections = Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(), + Collections = Collections?.Select(c => c.ToCollectionAccessSelection())?.ToList() ?? [], Groups = Groups }; From ef54bc814d5dfc4495e0b99de4d5adbb1d2a6d54 Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Fri, 26 Sep 2025 15:46:57 +0200 Subject: [PATCH 281/326] Fix a couple broken links found during self-onboarding (#6386) * Fix a couple broken links found during self-onboarding --- util/Migrator/README.md | 2 +- util/MySqlMigrations/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/util/Migrator/README.md b/util/Migrator/README.md index 950c2b682e..81c8bd3ae2 100644 --- a/util/Migrator/README.md +++ b/util/Migrator/README.md @@ -4,4 +4,4 @@ A class library leveraged by [utilities](../MsSqlMigratorUtility) and [hosted ap In production environments the Migrator is typically executed during application startup or as part of CI/CD pipelines to ensure database schemas are up-to-date before application deployment. -See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/mssql/) for how to utilize the files seen here. diff --git a/util/MySqlMigrations/README.md b/util/MySqlMigrations/README.md index 3373aabaee..1cace0c610 100644 --- a/util/MySqlMigrations/README.md +++ b/util/MySqlMigrations/README.md @@ -2,4 +2,4 @@ A class library leveraged by [hosted applications](/src/Admin/HostedServices/DatabaseMigrationHostedService.cs) to perform MySQL database migrations via Entity Framework. -See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/) for how to utilize the files seen here. +See the [documentation on creating migrations](https://contributing.bitwarden.com/contributing/database-migrations/ef/) for how to utilize the files seen here. From 9e0b767c98c6cdba5388fbf593f29891308300b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:56:28 -0500 Subject: [PATCH 282/326] [deps] Billing: Update CsvHelper to 33.1.0 (#6042) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> --- bitwarden_license/src/Commercial.Core/Commercial.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj b/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj index 57babb4043..9209917d1e 100644 --- a/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj +++ b/bitwarden_license/src/Commercial.Core/Commercial.Core.csproj @@ -5,7 +5,7 @@ - + From 80e7f4d85c0978e6108908c35bdc2fc2c760904e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 15:05:23 +0000 Subject: [PATCH 283/326] [deps] Billing: Update BenchmarkDotNet to 0.15.3 (#6041) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> --- perf/MicroBenchmarks/MicroBenchmarks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perf/MicroBenchmarks/MicroBenchmarks.csproj b/perf/MicroBenchmarks/MicroBenchmarks.csproj index 82c526a7d2..a13792b2d6 100644 --- a/perf/MicroBenchmarks/MicroBenchmarks.csproj +++ b/perf/MicroBenchmarks/MicroBenchmarks.csproj @@ -7,7 +7,7 @@ - + From b9e8b11311230b1176a161ebf194569cc8637279 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:05:56 -0700 Subject: [PATCH 284/326] update collections admin proc and repo (#6374) --- .../CollectionCipherRepository.cs | 13 ++- .../CollectionCipher_UpdateCollections.sql | 4 +- ...ollectionCipher_UpdateCollectionsAdmin.sql | 80 ++++++++++--------- ...ollectionCipher_UpdateCollectionsAdmin.sql | 55 +++++++++++++ 4 files changed, 109 insertions(+), 43 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-09-23_00_UpdateCollectionCipher_UpdateCollectionsAdmin.sql diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs index 6e2805f987..39e3ab8019 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionCipherRepository.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Microsoft.EntityFrameworkCore; @@ -145,9 +146,11 @@ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollec using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var availableCollections = await (from c in dbContext.Collections - where c.OrganizationId == organizationId - select c).ToListAsync(); + + var availableCollectionIds = await (from c in dbContext.Collections + where c.OrganizationId == organizationId + && c.Type != CollectionType.DefaultUserCollection + select c.Id).ToListAsync(); var currentCollectionCiphers = await (from cc in dbContext.CollectionCiphers where cc.CipherId == cipherId @@ -155,6 +158,8 @@ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollec foreach (var requestedCollectionId in collectionIds) { + if (!availableCollectionIds.Contains(requestedCollectionId)) continue; + var requestedCollectionCipher = currentCollectionCiphers .FirstOrDefault(cc => cc.CollectionId == requestedCollectionId); @@ -168,7 +173,7 @@ public class CollectionCipherRepository : BaseEntityFrameworkRepository, ICollec } } - dbContext.RemoveRange(currentCollectionCiphers.Where(cc => !collectionIds.Contains(cc.CollectionId))); + dbContext.RemoveRange(currentCollectionCiphers.Where(cc => availableCollectionIds.Contains(cc.CollectionId) && !collectionIds.Contains(cc.CollectionId))); await dbContext.UserBumpAccountRevisionDateByOrganizationIdAsync(organizationId); await dbContext.SaveChangesAsync(); } diff --git a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql index f3a1d964b5..2282524228 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollections.sql @@ -44,13 +44,13 @@ BEGIN [CollectionId], [CipherId] ) - SELECT + SELECT [Id], @CipherId FROM @CollectionIds WHERE [Id] IN (SELECT [Id] FROM [#TempAvailableCollections]) AND NOT EXISTS ( - SELECT 1 + SELECT 1 FROM [dbo].[CollectionCipher] WHERE [CollectionId] = [@CollectionIds].[Id] AND [CipherId] = @CipherId diff --git a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsAdmin.sql b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsAdmin.sql index 5f7b0215d9..1486709f09 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsAdmin.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionCipher_UpdateCollectionsAdmin.sql @@ -4,46 +4,52 @@ @CollectionIds AS [dbo].[GuidIdArray] READONLY AS BEGIN - SET NOCOUNT ON + SET NOCOUNT ON; - ;WITH [AvailableCollectionsCTE] AS( - SELECT - Id - FROM - [dbo].[Collection] - WHERE - OrganizationId = @OrganizationId - ), - [CollectionCiphersCTE] AS( - SELECT - [CollectionId], - [CipherId] - FROM - [dbo].[CollectionCipher] - WHERE - [CipherId] = @CipherId + -- Available collections for this org, excluding default collections + SELECT + C.[Id] + INTO #TempAvailableCollections + FROM [dbo].[Collection] AS C + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[Type] <> 1; -- exclude DefaultUserCollection + + -- Insert new collection assignments + INSERT INTO [dbo].[CollectionCipher] ( + [CollectionId], + [CipherId] ) - MERGE - [CollectionCiphersCTE] AS [Target] - USING - @CollectionIds AS [Source] - ON - [Target].[CollectionId] = [Source].[Id] - AND [Target].[CipherId] = @CipherId - WHEN NOT MATCHED BY TARGET - AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN - INSERT VALUES - ( - [Source].[Id], - @CipherId - ) - WHEN NOT MATCHED BY SOURCE - AND [Target].[CipherId] = @CipherId THEN - DELETE - ; + SELECT + S.[Id], + @CipherId + FROM @CollectionIds AS S + INNER JOIN #TempAvailableCollections AS A + ON A.[Id] = S.[Id] + WHERE NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionCipher] AS CC + WHERE CC.[CollectionId] = S.[Id] + AND CC.[CipherId] = @CipherId + ); + + -- Delete removed collection assignments + DELETE CC + FROM [dbo].[CollectionCipher] AS CC + INNER JOIN #TempAvailableCollections AS A + ON A.[Id] = CC.[CollectionId] + WHERE CC.[CipherId] = @CipherId + AND NOT EXISTS ( + SELECT 1 + FROM @CollectionIds AS S + WHERE S.[Id] = CC.[CollectionId] + ); IF @OrganizationId IS NOT NULL BEGIN - EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId; END -END \ No newline at end of file + + DROP TABLE #TempAvailableCollections; +END +GO diff --git a/util/Migrator/DbScripts/2025-09-23_00_UpdateCollectionCipher_UpdateCollectionsAdmin.sql b/util/Migrator/DbScripts/2025-09-23_00_UpdateCollectionCipher_UpdateCollectionsAdmin.sql new file mode 100644 index 0000000000..f69fdb30ff --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-23_00_UpdateCollectionCipher_UpdateCollectionsAdmin.sql @@ -0,0 +1,55 @@ +CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_UpdateCollectionsAdmin] + @CipherId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @CollectionIds AS [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON; + + -- Available collections for this org, excluding default collections + SELECT + C.[Id] + INTO #TempAvailableCollections + FROM [dbo].[Collection] AS C + WHERE + C.[OrganizationId] = @OrganizationId + AND C.[Type] <> 1; -- exclude DefaultUserCollection + + -- Insert new collection assignments + INSERT INTO [dbo].[CollectionCipher] ( + [CollectionId], + [CipherId] + ) + SELECT + S.[Id], + @CipherId + FROM @CollectionIds AS S + INNER JOIN #TempAvailableCollections AS A + ON A.[Id] = S.[Id] + WHERE NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionCipher] AS CC + WHERE CC.[CollectionId] = S.[Id] + AND CC.[CipherId] = @CipherId + ); + + -- Delete removed collection assignments + DELETE CC + FROM [dbo].[CollectionCipher] AS CC + INNER JOIN #TempAvailableCollections AS A + ON A.[Id] = CC.[CollectionId] + WHERE CC.[CipherId] = @CipherId + AND NOT EXISTS ( + SELECT 1 + FROM @CollectionIds AS S + WHERE S.[Id] = CC.[CollectionId] + ); + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId; + END + + DROP TABLE #TempAvailableCollections; +END +GO From 3a6b9564d5eda550dabdbd81b864382a6d67bdfa Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 26 Sep 2025 14:30:34 -0700 Subject: [PATCH 285/326] [PM-26004] - fix DeleteByOrganizationIdAsync_ExcludesDefaultCollectionCiphers test (#6389) * fix test * fix test --- .../Vault/Repositories/CipherRepositoryTests.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index ee8cd0247d..3a44453ed6 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -1290,12 +1290,16 @@ public class CipherRepositoryTests var cipherInBothCollections = await CreateOrgCipherAsync(); var unassignedCipher = await CreateOrgCipherAsync(); - await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInDefaultCollection.Id, organization.Id, - new List { defaultCollection.Id }); - await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInSharedCollection.Id, organization.Id, - new List { sharedCollection.Id }); - await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipherInBothCollections.Id, organization.Id, - new List { defaultCollection.Id, sharedCollection.Id }); + async Task LinkCollectionCipherAsync(Guid cipherId, Guid collectionId) => + await collectionCipherRepository.AddCollectionsForManyCiphersAsync( + organization.Id, + new[] { cipherId }, + new[] { collectionId }); + + await LinkCollectionCipherAsync(cipherInDefaultCollection.Id, defaultCollection.Id); + await LinkCollectionCipherAsync(cipherInSharedCollection.Id, sharedCollection.Id); + await LinkCollectionCipherAsync(cipherInBothCollections.Id, defaultCollection.Id); + await LinkCollectionCipherAsync(cipherInBothCollections.Id, sharedCollection.Id); await cipherRepository.DeleteByOrganizationIdAsync(organization.Id); @@ -1402,3 +1406,4 @@ public class CipherRepositoryTests Assert.Empty(remainingCollectionCiphers); } } + From 3dd4ee7a074b6ccf78ae40d519322e7a277baa8d Mon Sep 17 00:00:00 2001 From: Mick Letofsky Date: Mon, 29 Sep 2025 08:31:56 +0200 Subject: [PATCH 286/326] Create new Action for Claude code review of Vault Team code (#6379) Create new action for Claude Code Review of Vault Team Code. Worked to align what we have here with the initial `mcp-server` repo's code review action. --- .github/workflows/review-code.yml | 109 ++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .github/workflows/review-code.yml diff --git a/.github/workflows/review-code.yml b/.github/workflows/review-code.yml new file mode 100644 index 0000000000..b49f5cec8f --- /dev/null +++ b/.github/workflows/review-code.yml @@ -0,0 +1,109 @@ +name: Review code + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: {} + +jobs: + review: + name: Review + runs-on: ubuntu-24.04 + permissions: + contents: read + id-token: write + pull-requests: write + + steps: + - name: Check out repo + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check for Vault team changes + id: check_changes + run: | + # Ensure we have the base branch + 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) + + if [ -z "$CHANGED_FILES" ]; then + echo "Zero files changed" + echo "vault_team_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Handle variations in spacing and multiple teams + VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}') + + if [ -z "$VAULT_PATTERNS" ]; then + echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS" + echo "vault_team_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + vault_team_changes=false + for pattern in $VAULT_PATTERNS; do + echo "Checking pattern: $pattern" + + # Handle **/directory patterns + if [[ "$pattern" == "**/"* ]]; then + # Remove the **/ prefix + dir_pattern="${pattern#\*\*/}" + # Check if any file contains this directory in its path + if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then + vault_team_changes=true + echo "✅ Found files matching pattern: $pattern" + echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /' + break + fi + else + # Handle other patterns (shouldn't happen based on your CODEOWNERS) + if echo "$CHANGED_FILES" | grep -q "$pattern"; then + vault_team_changes=true + echo "✅ Found files matching pattern: $pattern" + echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /' + break + fi + fi + done + + echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT + + if [ "$vault_team_changes" = "true" ]; then + echo "" + echo "✅ Vault team changes detected - proceeding with review" + else + echo "" + echo "❌ No Vault team changes detected - skipping review" + fi + + - name: Review with Claude Code + if: steps.check_changes.outputs.vault_team_changes == 'true' + uses: anthropics/claude-code-action@a5528eec7426a4f0c9c1ac96018daa53ebd05bc4 # v1.0.7 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + track_progress: 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 }} + + Please review this pull request with a focus on: + - Code quality and best practices + - Potential bugs or issues + - Security implications + - Performance considerations + + Note: The PR branch is already checked out in the current working directory. + + Provide detailed feedback using inline comments for specific issues. + + claude_args: | + --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)" From a36340e9ad08920e85bed9c200135158fa1ed9f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:17:13 -0400 Subject: [PATCH 287/326] [deps]: Update prettier to v3.6.2 (#6212) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/MailTemplates/Mjml/package-lock.json | 8 ++++---- src/Core/MailTemplates/Mjml/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Core/MailTemplates/Mjml/package-lock.json b/src/Core/MailTemplates/Mjml/package-lock.json index a78405676f..df85185af9 100644 --- a/src/Core/MailTemplates/Mjml/package-lock.json +++ b/src/Core/MailTemplates/Mjml/package-lock.json @@ -14,7 +14,7 @@ }, "devDependencies": { "nodemon": "3.1.10", - "prettier": "3.5.3" + "prettier": "3.6.2" } }, "node_modules/@babel/runtime": { @@ -1564,9 +1564,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", "bin": { diff --git a/src/Core/MailTemplates/Mjml/package.json b/src/Core/MailTemplates/Mjml/package.json index c3690a2d73..8a8f81e845 100644 --- a/src/Core/MailTemplates/Mjml/package.json +++ b/src/Core/MailTemplates/Mjml/package.json @@ -25,6 +25,6 @@ }, "devDependencies": { "nodemon": "3.1.10", - "prettier": "3.5.3" + "prettier": "3.6.2" } } From e0ccd7f578e44f2eb31ec16d243ff95ce46ba29f Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Mon, 29 Sep 2025 13:06:52 -0400 Subject: [PATCH 288/326] chore(global-settings): [PM-24717] New Global Settings For New Device Verification - Updated secrets in the example secrets.json (#6387) --- dev/secrets.json.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/secrets.json.example b/dev/secrets.json.example index 7c91669b39..c6a16846e9 100644 --- a/dev/secrets.json.example +++ b/dev/secrets.json.example @@ -33,6 +33,8 @@ "id": "", "key": "" }, - "licenseDirectory": "" + "licenseDirectory": "", + "enableNewDeviceVerification": true, + "enableEmailVerification": true } } From f1af331a0c804d7f036f847e2fe4c274d700b082 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 29 Sep 2025 13:22:39 -0400 Subject: [PATCH 289/326] remove feature flag (#6395) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index ba8c1a84cd..44760c56a4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -133,7 +133,6 @@ public static class FeatureFlagKeys 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"; - public const string CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors"; /* Auth Team */ public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; From 46958cc838c43a8e702e6856eb30ea6250fdd222 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:06:52 -0500 Subject: [PATCH 290/326] [PM-25982] Restrict Ciphers being assigned to Default from Shared collections (#6382) * validate that any change in collection does not allow only shared ciphers to migrate to a default cipher * refactor order of checks to avoid any unnecessary calls * remove unneeded conditional --- .../Vault/Controllers/CiphersController.cs | 3 ++ src/Core/Vault/Services/ICipherService.cs | 1 + .../Services/Implementations/CipherService.cs | 50 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index c0a974bce2..06c88ad9bb 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -887,6 +887,9 @@ public class CiphersController : Controller [HttpPost("bulk-collections")] public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model) { + var userId = _userService.GetProperUserId(User).Value; + await _cipherService.ValidateBulkCollectionAssignmentAsync(model.CollectionIds, model.CipherIds, userId); + if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) || !await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds)) { diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index dac535433c..ffd79e9381 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -37,4 +37,5 @@ public interface ICipherService Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId); 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 ebfb2a4a2a..35b745fce6 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -170,6 +170,7 @@ public class CipherService : ICipherService { ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate); cipher.RevisionDate = DateTime.UtcNow; + await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId); await ValidateViewPasswordUserAsync(cipher); await _cipherRepository.ReplaceAsync(cipher); await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated); @@ -539,6 +540,7 @@ public class CipherService : ICipherService try { await ValidateCipherCanBeShared(cipher, sharingUserId, organizationId, lastKnownRevisionDate); + await ValidateChangeInCollectionsAsync(cipher, collectionIds, sharingUserId); // Sproc will not save this UserId on the cipher. It is used limit scope of the collectionIds. cipher.UserId = sharingUserId; @@ -678,6 +680,7 @@ public class CipherService : ICipherService { throw new BadRequestException("Cipher must belong to an organization."); } + await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId); cipher.RevisionDate = DateTime.UtcNow; @@ -820,6 +823,15 @@ public class CipherService : ICipherService return restoringCiphers; } + public async Task ValidateBulkCollectionAssignmentAsync(IEnumerable collectionIds, IEnumerable cipherIds, Guid userId) + { + foreach (var cipherId in cipherIds) + { + var cipher = await _cipherRepository.GetByIdAsync(cipherId); + await ValidateChangeInCollectionsAsync(cipher, collectionIds, userId); + } + } + private async Task UserCanEditAsync(Cipher cipher, Guid userId) { if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) @@ -1038,6 +1050,44 @@ public class CipherService : ICipherService } } + // Validates that a cipher is not being added to a default collection when it is only currently only in shared collections + private async Task ValidateChangeInCollectionsAsync(Cipher updatedCipher, IEnumerable newCollectionIds, Guid userId) + { + + if (updatedCipher.Id == Guid.Empty || !updatedCipher.OrganizationId.HasValue) + { + return; + } + + var currentCollectionsForCipher = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, updatedCipher.Id); + + if (!currentCollectionsForCipher.Any()) + { + // When a cipher is not currently in any collections it can be assigned to any type of collection + return; + } + + var currentCollections = await _collectionRepository.GetManyByManyIdsAsync(currentCollectionsForCipher.Select(c => c.CollectionId)); + + var currentCollectionsContainDefault = currentCollections.Any(c => c.Type == CollectionType.DefaultUserCollection); + + // When the current cipher already contains the default collection, no check is needed for if they added or removed + // a default collection, because it is already there. + if (currentCollectionsContainDefault) + { + return; + } + + var newCollections = await _collectionRepository.GetManyByManyIdsAsync(newCollectionIds); + var newCollectionsContainDefault = newCollections.Any(c => c.Type == CollectionType.DefaultUserCollection); + + if (newCollectionsContainDefault) + { + // User is trying to add the default collection when the cipher is only in shared collections + throw new BadRequestException("The cipher(s) cannot be assigned to a default collection when only assigned to non-default collections."); + } + } + private string SerializeCipherData(CipherData data) { return data switch From ca3d05c723c6b5a26284630b4017d3296cddfe91 Mon Sep 17 00:00:00 2001 From: Tyler <71953103+fntyler@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:40:20 -0400 Subject: [PATCH 291/326] BRE-1040 Dockerfiles shared ownership (#6257) * Include AppSec team and BRE dept for repository-level ownership of Dockerfile, and Dockerfile related, files. --- .github/CODEOWNERS | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f1c5f18fb..6db4905fec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,11 +4,12 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -## Docker files have shared ownership ## -**/Dockerfile -**/*.Dockerfile -**/.dockerignore -**/entrypoint.sh +## Docker-related files +**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre +**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre +**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre +**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre +**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre ## BRE team owns these workflows ## .github/workflows/publish.yml @bitwarden/dept-bre From f6b99a7906c6e9f50b498c55552fe75c6c3643d5 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:00:09 +0200 Subject: [PATCH 292/326] adds `pm-23995-no-logout-on-kdf-change` feature flag (#6397) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 44760c56a4..d26e0f67fa 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -186,6 +186,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 NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change"; /* Mobile Team */ public const string NativeCarouselFlow = "native-carousel-flow"; From 87849077365934743536d68685aee37a80b83532 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:29:18 -0700 Subject: [PATCH 293/326] chore(flag-removal): [Auth/PM20439] Remove Flagging Logic for BrowserExtensionLoginApproval (#6368) --- .../Controllers/AuthRequestsController.cs | 3 -- .../Utilities/LoginApprovingClientTypes.cs | 31 +++++-------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs index 4da3a2f491..e9dfe17c94 100644 --- a/src/Api/Auth/Controllers/AuthRequestsController.cs +++ b/src/Api/Auth/Controllers/AuthRequestsController.cs @@ -3,7 +3,6 @@ using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Request.AuthRequest; @@ -12,7 +11,6 @@ using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -55,7 +53,6 @@ public class AuthRequestsController( } [HttpGet("pending")] - [RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)] public async Task> GetPendingAuthRequestsAsync() { var userId = _userService.GetProperUserId(User).Value; diff --git a/src/Identity/Utilities/LoginApprovingClientTypes.cs b/src/Identity/Utilities/LoginApprovingClientTypes.cs index f0c7b831b7..28049ed16b 100644 --- a/src/Identity/Utilities/LoginApprovingClientTypes.cs +++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs @@ -1,6 +1,4 @@ -using Bit.Core; -using Bit.Core.Enums; -using Bit.Core.Services; +using Bit.Core.Enums; namespace Bit.Identity.Utilities; @@ -11,28 +9,15 @@ public interface ILoginApprovingClientTypes public class LoginApprovingClientTypes : ILoginApprovingClientTypes { - public LoginApprovingClientTypes( - IFeatureService featureService) + public LoginApprovingClientTypes() { - if (featureService.IsEnabled(FeatureFlagKeys.BrowserExtensionLoginApproval)) + TypesThatCanApprove = new List { - TypesThatCanApprove = new List - { - ClientType.Desktop, - ClientType.Mobile, - ClientType.Web, - ClientType.Browser, - }; - } - else - { - TypesThatCanApprove = new List - { - ClientType.Desktop, - ClientType.Mobile, - ClientType.Web, - }; - } + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }; } public IReadOnlyCollection TypesThatCanApprove { get; } From 718d96cc58d45f2b59ca7080da814a27f1bc7abf Mon Sep 17 00:00:00 2001 From: Alexey Zilber <110793805+alex8bitw@users.noreply.github.com> Date: Tue, 30 Sep 2025 12:28:30 +0800 Subject: [PATCH 294/326] Increased usable port range for ephemeral ports from 26,669 to 59,976 (#6394) --- src/Notifications/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Notifications/Dockerfile b/src/Notifications/Dockerfile index 1b0b507606..031df0b1b6 100644 --- a/src/Notifications/Dockerfile +++ b/src/Notifications/Dockerfile @@ -57,6 +57,8 @@ RUN apk add --no-cache curl \ WORKDIR /app COPY --from=build /source/src/Notifications/out /app COPY ./src/Notifications/entrypoint.sh /entrypoint.sh +RUN echo "net.ipv4.ip_local_port_range = 5024 65000" >> /etc/sysctl.d/99-sysctl.conf +RUN echo "net.ipv4.tcp_fin_timeout = 30" >> /etc/sysctl.d/99-sysctl.conf RUN chmod +x /entrypoint.sh HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1 From fc07dec3a69ae96f69c15f43bba37c35e9b7ee47 Mon Sep 17 00:00:00 2001 From: John Harrington <84741727+harr1424@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:43:43 -0700 Subject: [PATCH 295/326] PM-25915 tools exclude items in my items collections and my items collection from org vault export endpoint (#6362) Exclude MyItems and MyItems collection from Organizational Exports when CreateDefaultLocation feature flag is enabled --- .../OrganizationExportController.cs | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/Api/Tools/Controllers/OrganizationExportController.cs b/src/Api/Tools/Controllers/OrganizationExportController.cs index b1925dd3cf..dd039bc4a5 100644 --- a/src/Api/Tools/Controllers/OrganizationExportController.cs +++ b/src/Api/Tools/Controllers/OrganizationExportController.cs @@ -1,5 +1,6 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Tools.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -20,19 +21,22 @@ public class OrganizationExportController : Controller private readonly IAuthorizationService _authorizationService; private readonly IOrganizationCiphersQuery _organizationCiphersQuery; private readonly ICollectionRepository _collectionRepository; + private readonly IFeatureService _featureService; public OrganizationExportController( IUserService userService, GlobalSettings globalSettings, IAuthorizationService authorizationService, IOrganizationCiphersQuery organizationCiphersQuery, - ICollectionRepository collectionRepository) + ICollectionRepository collectionRepository, + IFeatureService featureService) { _userService = userService; _globalSettings = globalSettings; _authorizationService = authorizationService; _organizationCiphersQuery = organizationCiphersQuery; _collectionRepository = collectionRepository; + _featureService = featureService; } [HttpGet("export")] @@ -40,23 +44,47 @@ public class OrganizationExportController : Controller { var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), VaultExportOperations.ExportWholeVault); - if (canExportAll.Succeeded) - { - var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); - var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId); - return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections, _globalSettings)); - } - var canExportManaged = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId), VaultExportOperations.ExportManagedCollections); + var createDefaultLocationEnabled = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation); + + if (canExportAll.Succeeded) + { + if (createDefaultLocationEnabled) + { + var allOrganizationCiphers = + await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections( + organizationId); + + var allCollections = await _collectionRepository + .GetManySharedCollectionsByOrganizationIdAsync( + organizationId); + + + return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections, + _globalSettings)); + } + else + { + var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId); + + var allCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId); + + return Ok(new OrganizationExportResponseModel(allOrganizationCiphers, allCollections, + _globalSettings)); + } + } + if (canExportManaged.Succeeded) { var userId = _userService.GetProperUserId(User)!.Value; var allUserCollections = await _collectionRepository.GetManyByUserIdAsync(userId); - var managedOrgCollections = allUserCollections.Where(c => c.OrganizationId == organizationId && c.Manage).ToList(); - var managedCiphers = - await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId, managedOrgCollections.Select(c => c.Id)); + var managedOrgCollections = + allUserCollections.Where(c => c.OrganizationId == organizationId && c.Manage).ToList(); + + var managedCiphers = await _organizationCiphersQuery.GetOrganizationCiphersByCollectionIds(organizationId, + managedOrgCollections.Select(c => c.Id)); return Ok(new OrganizationExportResponseModel(managedCiphers, managedOrgCollections, _globalSettings)); } From 12303b3acf273e87acd681a701495cea970e5dc3 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:04:11 -0500 Subject: [PATCH 296/326] When deleting an archived clear the archived date so it will be restored to the vault (#6398) --- src/Core/Vault/Services/Implementations/CipherService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 35b745fce6..ca6bacd55a 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -721,6 +721,13 @@ public class CipherService : ICipherService cipherDetails.DeletedDate = cipherDetails.RevisionDate = DateTime.UtcNow; + if (cipherDetails.ArchivedDate.HasValue) + { + // If the cipher was archived, clear the archived date when soft deleting + // If a user were to restore an archived cipher, it should go back to the vault not the archive vault + cipherDetails.ArchivedDate = null; + } + await _cipherRepository.UpsertAsync(cipherDetails); await _eventService.LogCipherEventAsync(cipherDetails, EventType.Cipher_SoftDeleted); From 721fda0aaa5a68278ab6f5cc1b2ca80f87bd1a35 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:30:00 +0200 Subject: [PATCH 297/326] [PM-25473] Non-encryption passkeys prevent key rotation (#6359) * use webauthn credentials that have encrypted user key for user key rotation * where condition simplification --- .../WebAuthnLoginKeyRotationValidator.cs | 28 +++++---- src/Core/Auth/Entities/WebAuthnCredential.cs | 17 +++++ .../WebauthnLoginKeyRotationValidatorTests.cs | 62 ++++++++++++------- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs index 9c7efe0fbe..e92be11cd2 100644 --- a/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs @@ -1,4 +1,5 @@ using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -6,7 +7,13 @@ using Bit.Core.Exceptions; namespace Bit.Api.KeyManagement.Validators; -public class WebAuthnLoginKeyRotationValidator : IRotationValidator, IEnumerable> +/// +/// Validates WebAuthn credentials during key rotation. Only processes credentials that have PRF enabled +/// and have encrypted user, public, and private keys. Ensures all such credentials are included +/// in the rotation request with the required encrypted keys. +/// +public class WebAuthnLoginKeyRotationValidator : IRotationValidator, + IEnumerable> { private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; @@ -15,24 +22,20 @@ public class WebAuthnLoginKeyRotationValidator : IRotationValidator> ValidateAsync(User user, IEnumerable keysToRotate) + public async Task> ValidateAsync(User user, + IEnumerable keysToRotate) { var result = new List(); - var existing = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); - if (existing == null) + var validCredentials = (await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id)) + .Where(credential => credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled).ToList(); + if (validCredentials.Count == 0) { return result; } - var validCredentials = existing.Where(credential => credential.SupportsPrf); - if (!validCredentials.Any()) + foreach (var webAuthnCredential in validCredentials) { - return result; - } - - foreach (var ea in validCredentials) - { - var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == ea.Id); + var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id); if (keyToRotate == null) { throw new BadRequestException("All existing webauthn prf keys must be included in the rotation."); @@ -42,6 +45,7 @@ public class WebAuthnLoginKeyRotationValidator : IRotationValidator [MaxLength(20)] public string Type { get; set; } public Guid AaGuid { get; set; } + + /// + /// User key encrypted with this WebAuthn credential's public key (EncryptedPublicKey field). + /// [MaxLength(2000)] public string EncryptedUserKey { get; set; } + + /// + /// Private key encrypted with an external key for secure storage. + /// [MaxLength(2000)] public string EncryptedPrivateKey { get; set; } + + /// + /// Public key encrypted with the user key for key rotation. + /// [MaxLength(2000)] public string EncryptedPublicKey { get; set; } + + /// + /// Indicates whether this credential supports PRF (Pseudo-Random Function) extension. + /// public bool SupportsPrf { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs index 93652735ef..664a46bc9c 100644 --- a/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/WebauthnLoginKeyRotationValidatorTests.cs @@ -33,8 +33,9 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) .Returns(new List { data }); @@ -45,8 +46,12 @@ public class WebAuthnLoginKeyRotationValidatorTests } [Theory] - [BitAutoData] - public async Task ValidateAsync_DoesNotSupportPRF_Ignores( + [BitAutoData(false, null, null, null)] + [BitAutoData(true, null, "TestPublicKey", "TestPrivateKey")] + [BitAutoData(true, "TestUserKey", null, "TestPrivateKey")] + [BitAutoData(true, "TestUserKey", "TestPublicKey", null)] + public async Task ValidateAsync_NotEncryptedOrPrfNotSupported_Ignores( + bool supportsPrf, string encryptedUserKey, string encryptedPublicKey, string encryptedPrivateKey, SutProvider sutProvider, User user, IEnumerable webauthnRotateCredentialData) { @@ -58,7 +63,14 @@ public class WebAuthnLoginKeyRotationValidatorTests EncryptedPublicKey = e.EncryptedPublicKey, }).ToList(); - var data = new WebAuthnCredential { Id = guid, EncryptedUserKey = "Test", EncryptedPublicKey = "TestKey" }; + var data = new WebAuthnCredential + { + Id = guid, + SupportsPrf = supportsPrf, + EncryptedUserKey = encryptedUserKey, + EncryptedPublicKey = encryptedPublicKey, + EncryptedPrivateKey = encryptedPrivateKey + }; sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) .Returns(new List { data }); @@ -69,7 +81,7 @@ public class WebAuthnLoginKeyRotationValidatorTests [Theory] [BitAutoData] - public async Task ValidateAsync_WrongWebAuthnKeys_Throws( + public async Task ValidateAsync_WebAuthnKeysNotMatchingExisting_Throws( SutProvider sutProvider, User user, IEnumerable webauthnRotateCredentialData) { @@ -84,10 +96,12 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = Guid.Parse("00000000-0000-0000-0000-000000000002"), SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); @@ -100,20 +114,24 @@ public class WebAuthnLoginKeyRotationValidatorTests IEnumerable webauthnRotateCredentialData) { var guid = Guid.NewGuid(); - var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel - { - Id = guid, - EncryptedPublicKey = e.EncryptedPublicKey, - }).ToList(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => + new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedPublicKey = e.EncryptedPublicKey, + EncryptedUserKey = null + }).ToList(); var data = new WebAuthnCredential { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); @@ -131,19 +149,21 @@ public class WebAuthnLoginKeyRotationValidatorTests { Id = guid, EncryptedUserKey = e.EncryptedUserKey, + EncryptedPublicKey = null, }).ToList(); var data = new WebAuthnCredential { Id = guid, SupportsPrf = true, - EncryptedPublicKey = "TestKey", - EncryptedUserKey = "Test" + EncryptedPublicKey = "TestPublicKey", + EncryptedUserKey = "TestUserKey", + EncryptedPrivateKey = "TestPrivateKey" }; - sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id) + .Returns(new List { data }); await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); } - } From bca1d585c597a41525020386ce236b6139f5f9d0 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 1 Oct 2025 09:13:49 -0400 Subject: [PATCH 298/326] [SM-1489] machine account events (#6187) * Adding new logging for secrets * fixing secrest controller tests * fixing the tests * Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController * Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs * Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs * db changes * fixing the way we log events * Trying to undo some manual changes that should have been migrations * adding migration files * fixing test * setting up userid for project controller tests * adding sql * sql * Rename file * Trying to get it to for sure add the column before we try and update sprocs * Adding code to refresh the view to include ProjectId I hope * code improvements * Suggested changes * suggested changes * trying to fix sql issues * fixing swagger issue * Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * Suggested changes * Adding event logging for machine accounts * fixing two tests * trying to fix all tests * trying to fix tests * fixing test * Migrations * fix * updating eps * adding migration * Adding missing SQL changes * updating sql * fixing sql * running migration again * fixing sql * adding query to add grantedSErviceAccountId to event table * Suggested improvements * removing more migrations * more removal * removing all migrations to them redo them * redoing migration --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- .../CreateServiceAccountCommand.cs | 12 +- .../Controllers/EventsController.cs | 57 +- .../Models/Response/EventResponseModel.cs | 2 + .../Controllers/AccessPoliciesController.cs | 42 +- .../Controllers/ServiceAccountsController.cs | 20 +- src/Core/AdminConsole/Entities/Event.cs | 3 +- src/Core/AdminConsole/Enums/EventType.cs | 7 + .../AdminConsole/Models/Data/EventMessage.cs | 1 + .../Models/Data/EventTableEntity.cs | 16 +- src/Core/AdminConsole/Models/Data/IEvent.cs | 1 + .../Repositories/IEventRepository.cs | 1 + .../TableStorage/EventRepository.cs | 71 +- .../AdminConsole/Services/IEventService.cs | 4 + .../Services/Implementations/EventService.cs | 130 + .../NoopImplementations/NoopEventService.cs | 16 + .../EventEntityTypeConfiguration.cs | 13 +- ...geByOrganizationIdServiceAccountIdQuery.cs | 2 +- .../EventReadPageByServiceAccountIdQuery.cs | 48 + ...adPageByOrganizationIdServiceAccountId.sql | 2 +- .../Event_ReadPageByServiceAccountId.sql | 45 + .../dbo/Stored Procedures/Event_Create.sql | 9 +- src/Sql/dbo/Tables/Event.sql | 3 +- .../ServiceAccountsControllerTests.cs | 6 +- ...edMachineAccountEventLogsToEventSprocs.sql | 12 + ...edMachineAccountEventLogsToEventSprocs.sql | 189 + ...0250910211149_AddingMAEventLog.Designer.cs | 3278 ++++++++++++++++ .../20250910211149_AddingMAEventLog.cs | 28 + ...0926144434_AddingIndexToEvents.Designer.cs | 3284 ++++++++++++++++ .../20250926144434_AddingIndexToEvents.cs | 27 + .../DatabaseContextModelSnapshot.cs | 10 +- ...0250910211124_AddingMAEventLog.Designer.cs | 3284 ++++++++++++++++ .../20250910211124_AddingMAEventLog.cs | 27 + ...0926144506_AddingIndexToEvents.Designer.cs | 3290 +++++++++++++++++ .../20250926144506_AddingIndexToEvents.cs | 27 + .../DatabaseContextModelSnapshot.cs | 10 +- ...0250910211136_AddingMAEventLog.Designer.cs | 3267 ++++++++++++++++ .../20250910211136_AddingMAEventLog.cs | 27 + ...0926144450_AddingIndexToEvents.Designer.cs | 3273 ++++++++++++++++ .../20250926144450_AddingIndexToEvents.cs | 27 + .../DatabaseContextModelSnapshot.cs | 10 +- 40 files changed, 20553 insertions(+), 28 deletions(-) create mode 100644 src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs create mode 100644 src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByServiceAccountId.sql create mode 100644 util/Migrator/DbScripts/2025-08-26_00_AddGrantedMachineAccountEventLogsToEventSprocs.sql create mode 100644 util/Migrator/DbScripts/2025-08-26_01_AddGrantedMachineAccountEventLogsToEventSprocs.sql create mode 100644 util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.cs create mode 100644 util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.cs create mode 100644 util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.cs create mode 100644 util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.cs create mode 100644 util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.cs create mode 100644 util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.cs diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs index 12c7f679bd..b73b358925 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs @@ -1,10 +1,13 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts; @@ -13,15 +16,21 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand private readonly IAccessPolicyRepository _accessPolicyRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IServiceAccountRepository _serviceAccountRepository; + private readonly IEventService _eventService; + private readonly ICurrentContext _currentContext; public CreateServiceAccountCommand( IAccessPolicyRepository accessPolicyRepository, IOrganizationUserRepository organizationUserRepository, - IServiceAccountRepository serviceAccountRepository) + IServiceAccountRepository serviceAccountRepository, + IEventService eventService, + ICurrentContext currentContext) { _accessPolicyRepository = accessPolicyRepository; _organizationUserRepository = organizationUserRepository; _serviceAccountRepository = serviceAccountRepository; + _eventService = eventService; + _currentContext = currentContext; } public async Task CreateAsync(ServiceAccount serviceAccount, Guid userId) @@ -38,6 +47,7 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand Write = true, }; await _accessPolicyRepository.CreateManyAsync(new List { accessPolicy }); + await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType); return createdServiceAccount; } } diff --git a/src/Api/AdminConsole/Controllers/EventsController.cs b/src/Api/AdminConsole/Controllers/EventsController.cs index 18199ad8f2..f868f0b3b6 100644 --- a/src/Api/AdminConsole/Controllers/EventsController.cs +++ b/src/Api/AdminConsole/Controllers/EventsController.cs @@ -30,6 +30,8 @@ public class EventsController : Controller private readonly ICurrentContext _currentContext; private readonly ISecretRepository _secretRepository; private readonly IProjectRepository _projectRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; + public EventsController( IUserService userService, @@ -39,7 +41,8 @@ public class EventsController : Controller IEventRepository eventRepository, ICurrentContext currentContext, ISecretRepository secretRepository, - IProjectRepository projectRepository) + IProjectRepository projectRepository, + IServiceAccountRepository serviceAccountRepository) { _userService = userService; _cipherRepository = cipherRepository; @@ -49,6 +52,7 @@ public class EventsController : Controller _currentContext = currentContext; _secretRepository = secretRepository; _projectRepository = projectRepository; + _serviceAccountRepository = serviceAccountRepository; } [HttpGet("")] @@ -184,6 +188,57 @@ public class EventsController : Controller return new ListResponseModel(responses, result.ContinuationToken); } + [HttpGet("~/organization/{orgId}/service-account/{id}/events")] + public async Task> GetServiceAccounts( + Guid orgId, + Guid id, + [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, + [FromQuery] string continuationToken = null) + { + if (id == Guid.Empty || orgId == Guid.Empty) + { + throw new NotFoundException(); + } + + var serviceAccount = await GetServiceAccount(id, orgId); + var org = _currentContext.GetOrganization(orgId); + + if (org == null || !await _currentContext.AccessEventLogs(org.Id)) + { + throw new NotFoundException(); + } + + var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end); + var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync( + serviceAccount.OrganizationId, + serviceAccount.Id, + fromDate, + toDate, + new PageOptions { ContinuationToken = continuationToken }); + + var responses = result.Data.Select(e => new EventResponseModel(e)); + return new ListResponseModel(responses, result.ContinuationToken); + } + + [ApiExplorerSettings(IgnoreApi = true)] + private async Task GetServiceAccount(Guid serviceAccountId, Guid orgId) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId); + if (serviceAccount != null) + { + return serviceAccount; + } + + var fallbackServiceAccount = new ServiceAccount + { + Id = serviceAccountId, + OrganizationId = orgId + }; + + return fallbackServiceAccount; + } + [HttpGet("~/organizations/{orgId}/users/{id}/events")] public async Task> GetOrganizationUser(string orgId, string id, [FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null) diff --git a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs index bf02d8b00f..c259bc3bc4 100644 --- a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs @@ -35,6 +35,7 @@ public class EventResponseModel : ResponseModel SecretId = ev.SecretId; ProjectId = ev.ProjectId; ServiceAccountId = ev.ServiceAccountId; + GrantedServiceAccountId = ev.GrantedServiceAccountId; } public EventType Type { get; set; } @@ -58,4 +59,5 @@ public class EventResponseModel : ResponseModel public Guid? SecretId { get; set; } public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } + public Guid? GrantedServiceAccountId { get; set; } } diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index cd65a7cdf8..ad5d5e092b 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -29,6 +29,7 @@ public class AccessPoliciesController : Controller private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand; private readonly IUserService _userService; + private readonly IEventService _eventService; private readonly IProjectServiceAccountsAccessPoliciesUpdatesQuery _projectServiceAccountsAccessPoliciesUpdatesQuery; private readonly IUpdateProjectServiceAccountsAccessPoliciesCommand @@ -47,7 +48,8 @@ public class AccessPoliciesController : Controller IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery, IProjectServiceAccountsAccessPoliciesUpdatesQuery projectServiceAccountsAccessPoliciesUpdatesQuery, IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand, - IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand) + IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand, + IEventService eventService) { _authorizationService = authorizationService; _userService = userService; @@ -61,6 +63,7 @@ public class AccessPoliciesController : Controller _serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery; _projectServiceAccountsAccessPoliciesUpdatesQuery = projectServiceAccountsAccessPoliciesUpdatesQuery; _updateProjectServiceAccountsAccessPoliciesCommand = updateProjectServiceAccountsAccessPoliciesCommand; + _eventService = eventService; } [HttpGet("/organizations/{id}/access-policies/people/potential-grantees")] @@ -186,7 +189,9 @@ public class AccessPoliciesController : Controller } var userId = _userService.GetProperUserId(User)!.Value; + var currentPolicies = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId); var results = await _accessPolicyRepository.ReplaceServiceAccountPeopleAsync(peopleAccessPolicies, userId); + await LogAccessPolicyServiceAccountChanges(currentPolicies, results, userId); return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId); } @@ -336,4 +341,39 @@ public class AccessPoliciesController : Controller userId, accessClient); return new ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(results); } + + public async Task LogAccessPolicyServiceAccountChanges(IEnumerable currentPolicies, IEnumerable updatedPolicies, Guid userId) + { + foreach (var current in currentPolicies.OfType()) + { + if (!updatedPolicies.Any(r => r.Id == current.Id)) + { + await _eventService.LogServiceAccountGroupEventAsync(userId, current, EventType.ServiceAccount_GroupRemoved, _currentContext.IdentityClientType); + } + } + + foreach (var policy in updatedPolicies.OfType()) + { + if (!currentPolicies.Any(e => e.Id == policy.Id)) + { + await _eventService.LogServiceAccountGroupEventAsync(userId, policy, EventType.ServiceAccount_GroupAdded, _currentContext.IdentityClientType); + } + } + + foreach (var current in currentPolicies.OfType()) + { + if (!updatedPolicies.Any(r => r.Id == current.Id)) + { + await _eventService.LogServiceAccountPeopleEventAsync(userId, current, EventType.ServiceAccount_UserRemoved, _currentContext.IdentityClientType); + } + } + + foreach (var policy in updatedPolicies.OfType()) + { + if (!currentPolicies.Any(e => e.Id == policy.Id)) + { + await _eventService.LogServiceAccountPeopleEventAsync(userId, policy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType); + } + } + } } diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index 499c496cc9..0afdc3a1bf 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -42,6 +42,8 @@ public class ServiceAccountsController : Controller private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand; private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand; private readonly IPricingClient _pricingClient; + private readonly IEventService _eventService; + private readonly IOrganizationUserRepository _organizationUserRepository; public ServiceAccountsController( ICurrentContext currentContext, @@ -58,7 +60,9 @@ public class ServiceAccountsController : Controller IUpdateServiceAccountCommand updateServiceAccountCommand, IDeleteServiceAccountsCommand deleteServiceAccountsCommand, IRevokeAccessTokensCommand revokeAccessTokensCommand, - IPricingClient pricingClient) + IPricingClient pricingClient, + IEventService eventService, + IOrganizationUserRepository organizationUserRepository) { _currentContext = currentContext; _userService = userService; @@ -75,6 +79,8 @@ public class ServiceAccountsController : Controller _pricingClient = pricingClient; _createAccessTokenCommand = createAccessTokenCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; + _eventService = eventService; + _organizationUserRepository = organizationUserRepository; } [HttpGet("/organizations/{organizationId}/service-accounts")] @@ -139,8 +145,15 @@ public class ServiceAccountsController : Controller } var userId = _userService.GetProperUserId(User).Value; + var result = - await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId); + await _createServiceAccountCommand.CreateAsync(serviceAccount, userId); + + if (result != null) + { + await _eventService.LogServiceAccountEventAsync(userId, [serviceAccount], EventType.ServiceAccount_Created, _currentContext.IdentityClientType); + } + return new ServiceAccountResponseModel(result); } @@ -197,6 +210,9 @@ public class ServiceAccountsController : Controller } await _deleteServiceAccountsCommand.DeleteServiceAccounts(serviceAccountsToDelete); + var userId = _userService.GetProperUserId(User)!.Value; + await _eventService.LogServiceAccountEventAsync(userId, serviceAccountsToDelete, EventType.ServiceAccount_Deleted, _currentContext.IdentityClientType); + var responses = results.Select(r => new BulkDeleteResponseModel(r.ServiceAccount.Id, r.Error)); return new ListResponseModel(responses); } diff --git a/src/Core/AdminConsole/Entities/Event.cs b/src/Core/AdminConsole/Entities/Event.cs index 38d8f07b53..e2868c1915 100644 --- a/src/Core/AdminConsole/Entities/Event.cs +++ b/src/Core/AdminConsole/Entities/Event.cs @@ -34,6 +34,7 @@ public class Event : ITableObject, IEvent SecretId = e.SecretId; ProjectId = e.ProjectId; ServiceAccountId = e.ServiceAccountId; + GrantedServiceAccountId = e.GrantedServiceAccountId; } public Guid Id { get; set; } @@ -59,7 +60,7 @@ public class Event : ITableObject, IEvent public Guid? SecretId { get; set; } public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } - + public Guid? GrantedServiceAccountId { get; set; } public void SetNewId() { Id = CoreHelpers.GenerateComb(); diff --git a/src/Core/AdminConsole/Enums/EventType.cs b/src/Core/AdminConsole/Enums/EventType.cs index 81501fd6ec..c80dc2982c 100644 --- a/src/Core/AdminConsole/Enums/EventType.cs +++ b/src/Core/AdminConsole/Enums/EventType.cs @@ -109,4 +109,11 @@ public enum EventType : int Project_Created = 2201, Project_Edited = 2202, Project_Deleted = 2203, + + ServiceAccount_UserAdded = 2300, + ServiceAccount_UserRemoved = 2301, + ServiceAccount_GroupAdded = 2302, + ServiceAccount_GroupRemoved = 2303, + ServiceAccount_Created = 2304, + ServiceAccount_Deleted = 2305, } diff --git a/src/Core/AdminConsole/Models/Data/EventMessage.cs b/src/Core/AdminConsole/Models/Data/EventMessage.cs index b708c5bd56..a29d70c203 100644 --- a/src/Core/AdminConsole/Models/Data/EventMessage.cs +++ b/src/Core/AdminConsole/Models/Data/EventMessage.cs @@ -39,4 +39,5 @@ public class EventMessage : IEvent public Guid? SecretId { get; set; } public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } + public Guid? GrantedServiceAccountId { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs index 4ba50aee0d..1c3023f2cf 100644 --- a/src/Core/AdminConsole/Models/Data/EventTableEntity.cs +++ b/src/Core/AdminConsole/Models/Data/EventTableEntity.cs @@ -37,6 +37,7 @@ public class AzureEvent : ITableEntity public Guid? SecretId { get; set; } public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } + public Guid? GrantedServiceAccountId { get; set; } public EventTableEntity ToEventTableEntity() { @@ -68,6 +69,7 @@ public class AzureEvent : ITableEntity SecretId = SecretId, ServiceAccountId = ServiceAccountId, ProjectId = ProjectId, + GrantedServiceAccountId = GrantedServiceAccountId }; } } @@ -99,6 +101,7 @@ public class EventTableEntity : IEvent SecretId = e.SecretId; ProjectId = e.ProjectId; ServiceAccountId = e.ServiceAccountId; + GrantedServiceAccountId = e.GrantedServiceAccountId; } public string PartitionKey { get; set; } @@ -127,6 +130,7 @@ public class EventTableEntity : IEvent public Guid? SecretId { get; set; } public Guid? ProjectId { get; set; } public Guid? ServiceAccountId { get; set; } + public Guid? GrantedServiceAccountId { get; set; } public AzureEvent ToAzureEvent() { @@ -157,7 +161,8 @@ public class EventTableEntity : IEvent DomainName = DomainName, SecretId = SecretId, ProjectId = ProjectId, - ServiceAccountId = ServiceAccountId + ServiceAccountId = ServiceAccountId, + GrantedServiceAccountId = GrantedServiceAccountId }; } @@ -232,6 +237,15 @@ public class EventTableEntity : IEvent }); } + if (e.GrantedServiceAccountId.HasValue) + { + entities.Add(new EventTableEntity(e) + { + PartitionKey = pKey, + RowKey = $"GrantedServiceAccountId={e.GrantedServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}" + }); + } + return entities; } diff --git a/src/Core/AdminConsole/Models/Data/IEvent.cs b/src/Core/AdminConsole/Models/Data/IEvent.cs index 750fb2e2eb..3188c905e4 100644 --- a/src/Core/AdminConsole/Models/Data/IEvent.cs +++ b/src/Core/AdminConsole/Models/Data/IEvent.cs @@ -28,4 +28,5 @@ public interface IEvent Guid? SecretId { get; set; } Guid? ProjectId { get; set; } Guid? ServiceAccountId { get; set; } + Guid? GrantedServiceAccountId { get; set; } } diff --git a/src/Core/AdminConsole/Repositories/IEventRepository.cs b/src/Core/AdminConsole/Repositories/IEventRepository.cs index 281d6ec8c7..f0c185561b 100644 --- a/src/Core/AdminConsole/Repositories/IEventRepository.cs +++ b/src/Core/AdminConsole/Repositories/IEventRepository.cs @@ -27,6 +27,7 @@ public interface IEventRepository DateTime startDate, DateTime endDate, PageOptions pageOptions); Task> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions); + Task CreateAsync(IEvent e); Task CreateManyAsync(IEnumerable e); Task> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId, diff --git a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs index c9c803b5b2..169b36bf69 100644 --- a/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs +++ b/src/Core/AdminConsole/Repositories/TableStorage/EventRepository.cs @@ -77,12 +77,18 @@ public class EventRepository : IEventRepository return await GetManyAsync(partitionKey, $"CipherId={cipher.Id}__Date={{0}}", startDate, endDate, pageOptions); } - public async Task> GetManyByOrganizationServiceAccountAsync(Guid organizationId, - Guid serviceAccountId, DateTime startDate, DateTime endDate, PageOptions pageOptions) + public async Task> GetManyByOrganizationServiceAccountAsync( + Guid organizationId, + Guid serviceAccountId, + DateTime startDate, + DateTime endDate, + PageOptions pageOptions) { + return await GetManyServiceAccountAsync( + $"OrganizationId={organizationId}", + serviceAccountId.ToString(), + startDate, endDate, pageOptions); - return await GetManyAsync($"OrganizationId={organizationId}", - $"ServiceAccountId={serviceAccountId}__Date={{0}}", startDate, endDate, pageOptions); } public async Task CreateAsync(IEvent e) @@ -141,6 +147,40 @@ public class EventRepository : IEventRepository } } + public async Task> GetManyServiceAccountAsync( + string partitionKey, + string serviceAccountId, + DateTime startDate, + DateTime endDate, + PageOptions pageOptions) + { + var start = CoreHelpers.DateTimeToTableStorageKey(startDate); + var end = CoreHelpers.DateTimeToTableStorageKey(endDate); + var filter = MakeFilterForServiceAccount(partitionKey, serviceAccountId, startDate, endDate); + + var result = new PagedResult(); + var query = _tableClient.QueryAsync(filter, pageOptions.PageSize); + + await using (var enumerator = query.AsPages(pageOptions.ContinuationToken, + pageOptions.PageSize).GetAsyncEnumerator()) + { + if (await enumerator.MoveNextAsync()) + { + result.ContinuationToken = enumerator.Current.ContinuationToken; + + var events = enumerator.Current.Values + .Select(e => e.ToEventTableEntity()) + .ToList(); + + events = events.OrderByDescending(e => e.Date).ToList(); + + result.Data.AddRange(events); + } + } + + return result; + } + public async Task> GetManyAsync(string partitionKey, string rowKey, DateTime startDate, DateTime endDate, PageOptions pageOptions) { @@ -172,4 +212,27 @@ public class EventRepository : IEventRepository { return $"PartitionKey eq '{partitionKey}' and RowKey le '{rowStart}' and RowKey ge '{rowEnd}'"; } + + private string MakeFilterForServiceAccount( + string partitionKey, + string machineAccountId, + DateTime startDate, + DateTime endDate) + { + var start = CoreHelpers.DateTimeToTableStorageKey(startDate); + var end = CoreHelpers.DateTimeToTableStorageKey(endDate); + + var rowKey1Start = $"ServiceAccountId={machineAccountId}__Date={start}"; + var rowKey1End = $"ServiceAccountId={machineAccountId}__Date={end}"; + + var rowKey2Start = $"GrantedServiceAccountId={machineAccountId}__Date={start}"; + var rowKey2End = $"GrantedServiceAccountId={machineAccountId}__Date={end}"; + + var left = $"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey1Start}' and RowKey ge '{rowKey1End}'"; + var right = $"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey2Start}' and RowKey ge '{rowKey2End}'"; + + return $"({left}) or ({right})"; + } + + } diff --git a/src/Core/AdminConsole/Services/IEventService.cs b/src/Core/AdminConsole/Services/IEventService.cs index 80e8e63d8c..795c06e254 100644 --- a/src/Core/AdminConsole/Services/IEventService.cs +++ b/src/Core/AdminConsole/Services/IEventService.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Identity; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; @@ -37,4 +38,7 @@ public interface IEventService Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable secrets, EventType type, DateTime? date = null); Task LogUserProjectsEventAsync(Guid userId, IEnumerable projects, EventType type, DateTime? date = null); Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable projects, EventType type, DateTime? date = null); + Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null); + Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null); + Task LogServiceAccountEventAsync(Guid userId, List serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventService.cs b/src/Core/AdminConsole/Services/Implementations/EventService.cs index e0e0e040f1..77d481890e 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventService.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -516,6 +517,135 @@ public class EventService : IEventService await _eventWriteService.CreateManyAsync(eventMessages); } + + public async Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + var orgUser = await _organizationUserRepository.GetByIdAsync((Guid)policy.OrganizationUserId); + + if (!CanUseEvents(orgAbilities, orgUser.OrganizationId)) + { + return; + } + + var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType); + + if (actingUserId is null && serviceAccountId is null) + { + return; + } + + if (policy.OrganizationUserId != null) + { + var e = new EventMessage(_currentContext) + { + OrganizationId = orgUser.OrganizationId, + Type = type, + GrantedServiceAccountId = policy.GrantedServiceAccountId, + ServiceAccountId = serviceAccountId, + UserId = policy.OrganizationUserId, + ActingUserId = actingUserId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + + await _eventWriteService.CreateManyAsync(eventMessages); + } + } + + public async Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + + if (!CanUseEvents(orgAbilities, policy.Group.OrganizationId)) + { + return; + } + + var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType); + + if (actingUserId is null && serviceAccountId is null) + { + return; + } + + if (policy.GroupId != null) + { + var e = new EventMessage(_currentContext) + { + OrganizationId = policy.Group.OrganizationId, + Type = type, + GrantedServiceAccountId = policy.GrantedServiceAccountId, + ServiceAccountId = serviceAccountId, + GroupId = policy.GroupId, + ActingUserId = actingUserId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + + await _eventWriteService.CreateManyAsync(eventMessages); + } + } + + public async Task LogServiceAccountEventAsync(Guid userId, List serviceAccounts, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var eventMessages = new List(); + + foreach (var serviceAccount in serviceAccounts) + { + if (!CanUseEvents(orgAbilities, serviceAccount.OrganizationId)) + { + continue; + } + + var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType); + + if (actingUserId is null && serviceAccountId is null) + { + continue; + } + + if (serviceAccount != null) + { + var e = new EventMessage(_currentContext) + { + OrganizationId = serviceAccount.OrganizationId, + Type = type, + GrantedServiceAccountId = serviceAccount.Id, + ServiceAccountId = serviceAccountId, + ActingUserId = actingUserId, + Date = date.GetValueOrDefault(DateTime.UtcNow) + }; + eventMessages.Add(e); + } + } + + if (eventMessages.Any()) + { + await _eventWriteService.CreateManyAsync(eventMessages); + } + } + + private (Guid? actingUserId, Guid? serviceAccountId) MapIdentityClientType( + Guid userId, IdentityClientType identityClientType) + { + if (identityClientType == IdentityClientType.Organization) + { + return (null, null); + } + + return identityClientType switch + { + IdentityClientType.User => (userId, null), + IdentityClientType.ServiceAccount => (null, userId), + _ => throw new InvalidOperationException("Unknown identity client type.") + }; + } + + private async Task GetProviderIdAsync(Guid? orgId) { if (_currentContext == null || !orgId.HasValue) diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs index e8dd495205..6ecea7d234 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopEventService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Identity; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; @@ -139,4 +140,19 @@ public class NoopEventService : IEventService { return Task.FromResult(0); } + + public Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + return Task.FromResult(0); + } + + public Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + return Task.FromResult(0); + } + + public Task LogServiceAccountEventAsync(Guid userId, List serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null) + { + return Task.FromResult(0); + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs index 76e9b2e912..98f10394f4 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/EventEntityTypeConfiguration.cs @@ -12,9 +12,16 @@ public class EventEntityTypeConfiguration : IEntityTypeConfiguration .Property(e => e.Id) .ValueGeneratedNever(); - builder - .HasIndex(e => new { e.Date, e.OrganizationId, e.ActingUserId, e.CipherId }) - .IsClustered(false); + builder.HasKey(e => e.Id) + .IsClustered(); + + var index = builder.HasIndex(e => new { e.Date, e.OrganizationId, e.ActingUserId, e.CipherId }) + .IsClustered(false) + .HasDatabaseName("IX_Event_DateOrganizationIdUserId"); + + SqlServerIndexBuilderExtensions.IncludeProperties( + index, + e => new { e.ServiceAccountId, e.GrantedServiceAccountId }); builder.ToTable(nameof(Event)); } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs index 01f3a1fe14..72dc8db386 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByOrganizationIdServiceAccountIdQuery.cs @@ -30,7 +30,7 @@ public class EventReadPageByOrganizationIdServiceAccountIdQuery : IQuery (_beforeDate != null || e.Date <= _endDate) && (_beforeDate == null || e.Date < _beforeDate.Value) && e.OrganizationId == _organizationId && - e.ServiceAccountId == _serviceAccountId + (e.ServiceAccountId == _serviceAccountId || e.GrantedServiceAccountId == _serviceAccountId) orderby e.Date descending select e; return q.Skip(0).Take(_pageOptions.PageSize); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs new file mode 100644 index 0000000000..0d1cd6a656 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/EventReadPageByServiceAccountIdQuery.cs @@ -0,0 +1,48 @@ +using Bit.Core.Models.Data; +using Bit.Core.SecretsManager.Entities; +using Event = Bit.Infrastructure.EntityFramework.Models.Event; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class EventReadPageByServiceAccountQuery : IQuery +{ + private readonly ServiceAccount _serviceAccount; + private readonly DateTime _startDate; + private readonly DateTime _endDate; + private readonly DateTime? _beforeDate; + private readonly PageOptions _pageOptions; + + public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, PageOptions pageOptions) + { + _serviceAccount = serviceAccount; + _startDate = startDate; + _endDate = endDate; + _beforeDate = null; + _pageOptions = pageOptions; + } + + public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions) + { + _serviceAccount = serviceAccount; + _startDate = startDate; + _endDate = endDate; + _beforeDate = beforeDate; + _pageOptions = pageOptions; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var q = from e in dbContext.Events + where e.Date >= _startDate && + (_beforeDate == null || e.Date < _beforeDate.Value) && + ( + (_serviceAccount.OrganizationId == Guid.Empty && !e.OrganizationId.HasValue) || + (_serviceAccount.OrganizationId != Guid.Empty && e.OrganizationId == _serviceAccount.OrganizationId) + ) && + e.GrantedServiceAccountId == _serviceAccount.Id + orderby e.Date descending + select e; + + return q.Take(_pageOptions.PageSize); + } +} diff --git a/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql index 5dc950ffff..831c9f70ee 100644 --- a/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql +++ b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql @@ -18,7 +18,7 @@ BEGIN AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) AND [OrganizationId] = @OrganizationId - AND [ServiceAccountId] = @ServiceAccountId + AND ([ServiceAccountId] = @ServiceAccountId OR [GrantedServiceAccountId] = @ServiceAccountId) ORDER BY [Date] DESC OFFSET 0 ROWS FETCH NEXT @PageSize ROWS ONLY diff --git a/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByServiceAccountId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByServiceAccountId.sql new file mode 100644 index 0000000000..c429a4a064 --- /dev/null +++ b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByServiceAccountId.sql @@ -0,0 +1,45 @@ +CREATE PROCEDURE [dbo].[Event_ReadPageByServiceAccountId] + @GrantedServiceAccountId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId, + e.GrantedServiceAccountId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [GrantedServiceAccountId] = @GrantedServiceAccountId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY +END diff --git a/src/Sql/dbo/Stored Procedures/Event_Create.sql b/src/Sql/dbo/Stored Procedures/Event_Create.sql index 89971bd56f..0466bc1a69 100644 --- a/src/Sql/dbo/Stored Procedures/Event_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Event_Create.sql @@ -20,7 +20,8 @@ @DomainName VARCHAR(256), @SecretId UNIQUEIDENTIFIER = null, @ServiceAccountId UNIQUEIDENTIFIER = null, - @ProjectId UNIQUEIDENTIFIER = null + @ProjectId UNIQUEIDENTIFIER = null, + @GrantedServiceAccountId UNIQUEIDENTIFIER = null AS BEGIN SET NOCOUNT ON @@ -48,7 +49,8 @@ BEGIN [DomainName], [SecretId], [ServiceAccountId], - [ProjectId] + [ProjectId], + [GrantedServiceAccountId] ) VALUES ( @@ -73,6 +75,7 @@ BEGIN @DomainName, @SecretId, @ServiceAccountId, - @ProjectId + @ProjectId, + @GrantedServiceAccountId ) END diff --git a/src/Sql/dbo/Tables/Event.sql b/src/Sql/dbo/Tables/Event.sql index 6dfb4392a0..ea0dda5661 100644 --- a/src/Sql/dbo/Tables/Event.sql +++ b/src/Sql/dbo/Tables/Event.sql @@ -21,11 +21,12 @@ [SecretId] UNIQUEIDENTIFIER NULL, [ServiceAccountId] UNIQUEIDENTIFIER NULL, [ProjectId] UNIQUEIDENTIFIER NULL, + [GrantedServiceAccountId] UNIQUEIDENTIFIER NULL, CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC) ); GO CREATE NONCLUSTERED INDEX [IX_Event_DateOrganizationIdUserId] - ON [dbo].[Event]([Date] DESC, [OrganizationId] ASC, [ActingUserId] ASC, [CipherId] ASC); + ON [dbo].[Event]([Date] DESC, [OrganizationId] ASC, [ActingUserId] ASC, [CipherId] ASC) INCLUDE ([ServiceAccountId], [GrantedServiceAccountId]); diff --git a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 8147b81240..78224a8bd8 100644 --- a/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -361,7 +361,7 @@ public class ServiceAccountsControllerTests [Theory] [BitAutoData] - public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider sutProvider, List data) + public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider sutProvider, List data, Guid userId) { var ids = data.Select(sa => sa.Id).ToList(); var organizationId = data.First().OrganizationId; @@ -377,6 +377,7 @@ public class ServiceAccountsControllerTests Arg.Any>()).Returns(AuthorizationResult.Failed()); sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); var results = await sutProvider.Sut.BulkDeleteAsync(ids); @@ -390,7 +391,7 @@ public class ServiceAccountsControllerTests [Theory] [BitAutoData] - public async Task BulkDelete_Success(SutProvider sutProvider, List data) + public async Task BulkDelete_Success(SutProvider sutProvider, List data, Guid userId) { var ids = data.Select(sa => sa.Id).ToList(); var organizationId = data.First().OrganizationId; @@ -404,6 +405,7 @@ public class ServiceAccountsControllerTests sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); var results = await sutProvider.Sut.BulkDeleteAsync(ids); diff --git a/util/Migrator/DbScripts/2025-08-26_00_AddGrantedMachineAccountEventLogsToEventSprocs.sql b/util/Migrator/DbScripts/2025-08-26_00_AddGrantedMachineAccountEventLogsToEventSprocs.sql new file mode 100644 index 0000000000..a2caeeab77 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-26_00_AddGrantedMachineAccountEventLogsToEventSprocs.sql @@ -0,0 +1,12 @@ +IF COL_LENGTH('[dbo].[Event]', 'GrantedServiceAccountId') IS NULL +BEGIN + ALTER TABLE [dbo].[Event] + ADD [GrantedServiceAccountId] UNIQUEIDENTIFIER NULL; +END +GO + +IF OBJECT_ID('[dbo].[EventView]', 'V') IS NOT NULL +BEGIN + EXECUTE sp_refreshview N'[dbo].[EventView]' +END +GO diff --git a/util/Migrator/DbScripts/2025-08-26_01_AddGrantedMachineAccountEventLogsToEventSprocs.sql b/util/Migrator/DbScripts/2025-08-26_01_AddGrantedMachineAccountEventLogsToEventSprocs.sql new file mode 100644 index 0000000000..10cc771fc5 --- /dev/null +++ b/util/Migrator/DbScripts/2025-08-26_01_AddGrantedMachineAccountEventLogsToEventSprocs.sql @@ -0,0 +1,189 @@ +-- Create or alter Event_Create procedure +CREATE OR ALTER PROCEDURE [dbo].[Event_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Type INT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @PolicyId UNIQUEIDENTIFIER, + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserId UNIQUEIDENTIFIER, + @ProviderUserId UNIQUEIDENTIFIER, + @ProviderOrganizationId UNIQUEIDENTIFIER = NULL, + @ActingUserId UNIQUEIDENTIFIER, + @DeviceType SMALLINT, + @IpAddress VARCHAR(50), + @Date DATETIME2(7), + @SystemUser TINYINT = NULL, + @DomainName VARCHAR(256), + @SecretId UNIQUEIDENTIFIER = NULL, + @ServiceAccountId UNIQUEIDENTIFIER = NULL, + @ProjectId UNIQUEIDENTIFIER = NULL, + @GrantedServiceAccountId UNIQUEIDENTIFIER = NULL +AS +BEGIN + SET NOCOUNT ON; + + INSERT INTO [dbo].[Event] + ( + [Id], + [Type], + [UserId], + [OrganizationId], + [InstallationId], + [ProviderId], + [CipherId], + [CollectionId], + [PolicyId], + [GroupId], + [OrganizationUserId], + [ProviderUserId], + [ProviderOrganizationId], + [ActingUserId], + [DeviceType], + [IpAddress], + [Date], + [SystemUser], + [DomainName], + [SecretId], + [ServiceAccountId], + [ProjectId], + [GrantedServiceAccountId] + ) + VALUES + ( + @Id, + @Type, + @UserId, + @OrganizationId, + @InstallationId, + @ProviderId, + @CipherId, + @CollectionId, + @PolicyId, + @GroupId, + @OrganizationUserId, + @ProviderUserId, + @ProviderOrganizationId, + @ActingUserId, + @DeviceType, + @IpAddress, + @Date, + @SystemUser, + @DomainName, + @SecretId, + @ServiceAccountId, + @ProjectId, + @GrantedServiceAccountId + ); +END +GO + +-- Create or alter Event_ReadPageByServiceAccountId procedure +CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageByServiceAccountId] + @GrantedServiceAccountId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON; + + SELECT + e.Id, + e.Date, + e.Type, + e.UserId, + e.OrganizationId, + e.InstallationId, + e.ProviderId, + e.CipherId, + e.CollectionId, + e.PolicyId, + e.GroupId, + e.OrganizationUserId, + e.ProviderUserId, + e.ProviderOrganizationId, + e.DeviceType, + e.IpAddress, + e.ActingUserId, + e.SystemUser, + e.DomainName, + e.SecretId, + e.ServiceAccountId, + e.ProjectId, + e.GrantedServiceAccountId + FROM + [dbo].[EventView] e + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [GrantedServiceAccountId] = @GrantedServiceAccountId + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageByOrganizationIdServiceAccountId] + @OrganizationId UNIQUEIDENTIFIER, + @ServiceAccountId UNIQUEIDENTIFIER, + @StartDate DATETIME2(7), + @EndDate DATETIME2(7), + @BeforeDate DATETIME2(7), + @PageSize INT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[EventView] + WHERE + [Date] >= @StartDate + AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate) + AND (@BeforeDate IS NULL OR [Date] < @BeforeDate) + AND [OrganizationId] = @OrganizationId + AND ([ServiceAccountId] = @ServiceAccountId OR [GrantedServiceAccountId] = @ServiceAccountId) + ORDER BY [Date] DESC + OFFSET 0 ROWS + FETCH NEXT @PageSize ROWS ONLY +END +GO + +IF EXISTS(SELECT 1 FROM sys.indexes WHERE name = 'IX_Event_DateOrganizationIdUserId') +BEGIN + -- Check if neither ServiceAccountId nor GrantedServiceAccountId are included columns + IF NOT EXISTS ( + SELECT 1 + FROM + sys.indexes i + INNER JOIN + sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id + INNER JOIN + sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + WHERE + i.object_id = OBJECT_ID('[dbo].[Event]') + AND i.name = 'IX_Event_DateOrganizationIdUserId' + AND c.name IN ('ServiceAccountId', 'GrantedServiceAccountId') + AND ic.is_included_column = 1 + ) + BEGIN + CREATE NONCLUSTERED INDEX [IX_Event_DateOrganizationIdUserId] + ON [dbo].[Event] + ( [Date] DESC, + [OrganizationId] ASC, + [ActingUserId] ASC, + [CipherId] ASC + ) + INCLUDE ([ServiceAccountId], [GrantedServiceAccountId]) + WITH (DROP_EXISTING = ON) + END +END +GO \ No newline at end of file diff --git a/util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.Designer.cs b/util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.Designer.cs new file mode 100644 index 0000000000..7bfe9caace --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.Designer.cs @@ -0,0 +1,3278 @@ +// +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("20250910211149_AddingMAEventLog")] + partial class AddingMAEventLog + { + /// + 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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250910211149_AddingMAEventLog.cs b/util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.cs new file mode 100644 index 0000000000..ad268f0dc8 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250910211149_AddingMAEventLog.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddingMAEventLog : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GrantedServiceAccountId", + table: "Event", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GrantedServiceAccountId", + table: "Event"); + } +} diff --git a/util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.Designer.cs b/util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.Designer.cs new file mode 100644 index 0000000000..6dcc834ece --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.Designer.cs @@ -0,0 +1,3284 @@ +// +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("20250926144434_AddingIndexToEvents")] + partial class AddingIndexToEvents + { + /// + 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.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/20250926144434_AddingIndexToEvents.cs b/util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.cs new file mode 100644 index 0000000000..162f7d2954 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250926144434_AddingIndexToEvents.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddingIndexToEvents : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_Date_OrganizationId_ActingUserId_CipherId", + table: "Event", + newName: "IX_Event_DateOrganizationIdUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_DateOrganizationIdUserId", + table: "Event", + newName: "IX_Event_Date_OrganizationId_ActingUserId_CipherId"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index beab31cebf..dce61f805c 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1282,6 +1282,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("DomainName") .HasColumnType("longtext"); + b.Property("GrantedServiceAccountId") + .HasColumnType("char(36)"); + b.Property("GroupId") .HasColumnType("char(36)"); @@ -1328,10 +1331,13 @@ namespace Bit.MySqlMigrations.Migrations b.Property("UserId") .HasColumnType("char(36)"); - b.HasKey("Id"); + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") - .HasAnnotation("SqlServer:Clustered", false); + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); b.ToTable("Event", (string)null); }); diff --git a/util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.Designer.cs b/util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.Designer.cs new file mode 100644 index 0000000000..5a85eb6144 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.Designer.cs @@ -0,0 +1,3284 @@ +// +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("20250910211124_AddingMAEventLog")] + partial class AddingMAEventLog + { + /// + 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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250910211124_AddingMAEventLog.cs b/util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.cs new file mode 100644 index 0000000000..5dbaef5950 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250910211124_AddingMAEventLog.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddingMAEventLog : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GrantedServiceAccountId", + table: "Event", + type: "uuid", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GrantedServiceAccountId", + table: "Event"); + } +} diff --git a/util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.Designer.cs b/util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.Designer.cs new file mode 100644 index 0000000000..4f7cd718ea --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.Designer.cs @@ -0,0 +1,3290 @@ +// +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("20250926144506_AddingIndexToEvents")] + partial class AddingIndexToEvents + { + /// + 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.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/20250926144506_AddingIndexToEvents.cs b/util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.cs new file mode 100644 index 0000000000..876d2b9347 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250926144506_AddingIndexToEvents.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddingIndexToEvents : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_Date_OrganizationId_ActingUserId_CipherId", + table: "Event", + newName: "IX_Event_DateOrganizationIdUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_DateOrganizationIdUserId", + table: "Event", + newName: "IX_Event_Date_OrganizationId_ActingUserId_CipherId"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 3d66e2c035..c6ed007410 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1287,6 +1287,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("DomainName") .HasColumnType("text"); + b.Property("GrantedServiceAccountId") + .HasColumnType("uuid"); + b.Property("GroupId") .HasColumnType("uuid"); @@ -1333,10 +1336,13 @@ namespace Bit.PostgresMigrations.Migrations b.Property("UserId") .HasColumnType("uuid"); - b.HasKey("Id"); + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") - .HasAnnotation("SqlServer:Clustered", false); + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); b.ToTable("Event", (string)null); }); diff --git a/util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.Designer.cs b/util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.Designer.cs new file mode 100644 index 0000000000..43091d1136 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.Designer.cs @@ -0,0 +1,3267 @@ +// +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("20250910211136_AddingMAEventLog")] + partial class AddingMAEventLog + { + /// + 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"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + 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("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/20250910211136_AddingMAEventLog.cs b/util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.cs new file mode 100644 index 0000000000..0022934414 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250910211136_AddingMAEventLog.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddingMAEventLog : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GrantedServiceAccountId", + table: "Event", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GrantedServiceAccountId", + table: "Event"); + } +} diff --git a/util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.Designer.cs b/util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.Designer.cs new file mode 100644 index 0000000000..6473c5a3f1 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.Designer.cs @@ -0,0 +1,3273 @@ +// +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("20250926144450_AddingIndexToEvents")] + partial class AddingIndexToEvents + { + /// + 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.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/20250926144450_AddingIndexToEvents.cs b/util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.cs new file mode 100644 index 0000000000..65de0c9ef1 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250926144450_AddingIndexToEvents.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddingIndexToEvents : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_Date_OrganizationId_ActingUserId_CipherId", + table: "Event", + newName: "IX_Event_DateOrganizationIdUserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameIndex( + name: "IX_Event_DateOrganizationIdUserId", + table: "Event", + newName: "IX_Event_Date_OrganizationId_ActingUserId_CipherId"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index d091cb4830..494431b932 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1271,6 +1271,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("DomainName") .HasColumnType("TEXT"); + b.Property("GrantedServiceAccountId") + .HasColumnType("TEXT"); + b.Property("GroupId") .HasColumnType("TEXT"); @@ -1317,10 +1320,13 @@ namespace Bit.SqliteMigrations.Migrations b.Property("UserId") .HasColumnType("TEXT"); - b.HasKey("Id"); + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") - .HasAnnotation("SqlServer:Clustered", false); + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); b.ToTable("Event", (string)null); }); From 7cefca330b856e04f1e9efc16ff95b3824f476df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:28:19 +0100 Subject: [PATCH 299/326] [PM-26050] Migrate all DefaultUserCollection when claimed user is deleted (#6366) * feat: migrate DefaultUserCollection to SharedCollection during user deletion - Implemented migration of DefaultUserCollection to SharedCollection in EF UserRepository before deleting organization users. - Updated stored procedures User_DeleteById and User_DeleteByIds to include migration logic. - Added new migration script for updating stored procedures. * Add unit test for user deletion and DefaultUserCollection migration - Implemented a new test to verify the migration of DefaultUserCollection to SharedCollection during user deletion in UserRepository. - The test ensures that the user is deleted and the associated collection is updated correctly. * Refactor user deletion process in UserRepository - Moved migrating DefaultUserCollection to SharedCollection to happen before the deletion of user-related entities. - Updated the deletion logic to use ExecuteDeleteAsync for improved performance and clarity. - Ensured that all related entities are removed in a single transaction to maintain data integrity. * Add unit test for DeleteManyAsync in UserRepository - Implemented a new test to verify the deletion of multiple users and the migration of their DefaultUserCollections to SharedCollections. - Ensured that both users are deleted and their associated collections are updated correctly in a single transaction. * Refactor UserRepositoryTests to use test user creation methods and streamline collection creation * Ensure changes are saved after deleting users in bulk * Refactor UserRepository to simplify migration queries and remove unnecessary loops for better performance * Refactor UserRepository to encapsulate DefaultUserCollection migration logic in a separate method * Refactor UserRepository to optimize deletion queries by using joins instead of subqueries for improved performance * Refactor UserRepositoryTest DeleteManyAsync_Works to ensure GroupUser and CollectionUser deletion --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .../Repositories/UserRepository.cs | 62 +++- .../dbo/Stored Procedures/User_DeleteById.sql | 10 + .../Stored Procedures/User_DeleteByIds.sql | 10 + .../Auth/Repositories/UserRepositoryTests.cs | 163 ++++++--- ..._MigrateDefaultCollectionsOnUserDelete.sql | 325 ++++++++++++++++++ 5 files changed, 512 insertions(+), 58 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-09-23_00_MigrateDefaultCollectionsOnUserDelete.sql diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index bd70e27e78..809704edb7 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -283,6 +283,9 @@ public class UserRepository : Repository, IUserR var transaction = await dbContext.Database.BeginTransactionAsync(); + MigrateDefaultUserCollectionsToShared(dbContext, [user.Id]); + await dbContext.SaveChangesAsync(); + dbContext.WebAuthnCredentials.RemoveRange(dbContext.WebAuthnCredentials.Where(w => w.UserId == user.Id)); dbContext.Ciphers.RemoveRange(dbContext.Ciphers.Where(c => c.UserId == user.Id)); dbContext.Folders.RemoveRange(dbContext.Folders.Where(f => f.UserId == user.Id)); @@ -314,8 +317,8 @@ public class UserRepository : Repository, IUserR var mappedUser = Mapper.Map(user); dbContext.Users.Remove(mappedUser); - await transaction.CommitAsync(); await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); } } @@ -329,21 +332,30 @@ public class UserRepository : Repository, IUserR var targetIds = users.Select(u => u.Id).ToList(); + MigrateDefaultUserCollectionsToShared(dbContext, targetIds); + await dbContext.SaveChangesAsync(); + await dbContext.WebAuthnCredentials.Where(wa => targetIds.Contains(wa.UserId)).ExecuteDeleteAsync(); await dbContext.Ciphers.Where(c => targetIds.Contains(c.UserId ?? default)).ExecuteDeleteAsync(); await dbContext.Folders.Where(f => targetIds.Contains(f.UserId)).ExecuteDeleteAsync(); await dbContext.AuthRequests.Where(a => targetIds.Contains(a.UserId)).ExecuteDeleteAsync(); await dbContext.Devices.Where(d => targetIds.Contains(d.UserId)).ExecuteDeleteAsync(); - var collectionUsers = from cu in dbContext.CollectionUsers - join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id - where targetIds.Contains(ou.UserId ?? default) - select cu; - dbContext.CollectionUsers.RemoveRange(collectionUsers); - var groupUsers = from gu in dbContext.GroupUsers - join ou in dbContext.OrganizationUsers on gu.OrganizationUserId equals ou.Id - where targetIds.Contains(ou.UserId ?? default) - select gu; - dbContext.GroupUsers.RemoveRange(groupUsers); + await dbContext.CollectionUsers + .Join(dbContext.OrganizationUsers, + cu => cu.OrganizationUserId, + ou => ou.Id, + (cu, ou) => new { CollectionUser = cu, OrganizationUser = ou }) + .Where((joined) => targetIds.Contains(joined.OrganizationUser.UserId ?? default)) + .Select(joined => joined.CollectionUser) + .ExecuteDeleteAsync(); + await dbContext.GroupUsers + .Join(dbContext.OrganizationUsers, + gu => gu.OrganizationUserId, + ou => ou.Id, + (gu, ou) => new { GroupUser = gu, OrganizationUser = ou }) + .Where(joined => targetIds.Contains(joined.OrganizationUser.UserId ?? default)) + .Select(joined => joined.GroupUser) + .ExecuteDeleteAsync(); await dbContext.UserProjectAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync(); await dbContext.UserServiceAccountAccessPolicy.Where(ap => targetIds.Contains(ap.OrganizationUser.UserId ?? default)).ExecuteDeleteAsync(); await dbContext.OrganizationUsers.Where(ou => targetIds.Contains(ou.UserId ?? default)).ExecuteDeleteAsync(); @@ -354,15 +366,29 @@ public class UserRepository : Repository, IUserR await dbContext.NotificationStatuses.Where(ns => targetIds.Contains(ns.UserId)).ExecuteDeleteAsync(); await dbContext.Notifications.Where(n => targetIds.Contains(n.UserId ?? default)).ExecuteDeleteAsync(); - foreach (var u in users) - { - var mappedUser = Mapper.Map(u); - dbContext.Users.Remove(mappedUser); - } + await dbContext.Users.Where(u => targetIds.Contains(u.Id)).ExecuteDeleteAsync(); - - await transaction.CommitAsync(); await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + } + } + + private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable userIds) + { + var defaultCollections = (from c in dbContext.Collections + join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId + join ou in dbContext.OrganizationUsers on cu.OrganizationUserId equals ou.Id + join u in dbContext.Users on ou.UserId equals u.Id + where userIds.Contains(ou.UserId!.Value) + && c.Type == Core.Enums.CollectionType.DefaultUserCollection + select new { Collection = c, UserEmail = u.Email }) + .ToList(); + + foreach (var item in defaultCollections) + { + item.Collection.Type = Core.Enums.CollectionType.SharedCollection; + item.Collection.DefaultUserCollectionEmail = item.Collection.DefaultUserCollectionEmail ?? item.UserEmail; + item.Collection.RevisionDate = DateTime.UtcNow; } } } diff --git a/src/Sql/dbo/Stored Procedures/User_DeleteById.sql b/src/Sql/dbo/Stored Procedures/User_DeleteById.sql index 0608982e37..6377166e17 100644 --- a/src/Sql/dbo/Stored Procedures/User_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/User_DeleteById.sql @@ -52,6 +52,16 @@ BEGIN WHERE [UserId] = @Id + -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records + DECLARE @OrgUserIds [dbo].[GuidIdArray] + INSERT INTO @OrgUserIds (Id) + SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] = @Id + + IF EXISTS (SELECT 1 FROM @OrgUserIds) + BEGIN + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds + END + -- Delete collection users DELETE CU diff --git a/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql index 97ab955f83..cdf3dd7d3a 100644 --- a/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql +++ b/src/Sql/dbo/Stored Procedures/User_DeleteByIds.sql @@ -66,6 +66,16 @@ BEGIN WHERE [UserId] IN (SELECT * FROM @ParsedIds) + -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records + DECLARE @OrgUserIds [dbo].[GuidIdArray] + INSERT INTO @OrgUserIds (Id) + SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] IN (SELECT * FROM @ParsedIds) + + IF EXISTS (SELECT 1 FROM @OrgUserIds) + BEGIN + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds + END + -- Delete collection users DELETE CU diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs index 0bf0909a0a..dd84df07be 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs @@ -1,7 +1,9 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Repositories; +using Bit.Infrastructure.IntegrationTest.AdminConsole; using Xunit; namespace Bit.Infrastructure.IntegrationTest.Repositories; @@ -25,53 +27,40 @@ public class UserRepositoryTests Assert.Null(deletedUser); } - [DatabaseTheory, DatabaseData] - public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository) + [Theory, DatabaseData] + public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IGroupRepository groupRepository) { - var user1 = await userRepository.CreateAsync(new User - { - Name = "Test User 1", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + var user3 = await userRepository.CreateTestUserAsync(); - var user2 = await userRepository.CreateAsync(new User - { - Name = "Test User 2", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3); - var user3 = await userRepository.CreateAsync(new User - { - Name = "Test User 3", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); + var group1 = await groupRepository.CreateTestGroupAsync(organization, "test-group-1"); + var group2 = await groupRepository.CreateTestGroupAsync(organization, "test-group-2"); + await groupRepository.UpdateUsersAsync(group1.Id, [orgUser1.Id]); + await groupRepository.UpdateUsersAsync(group2.Id, [orgUser3.Id]); - var organization = await organizationRepository.CreateAsync(new Organization - { - Name = "Test Org", - BillingEmail = user3.Email, // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser + var collection1 = new Collection { OrganizationId = organization.Id, - UserId = user1.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - await organizationUserRepository.CreateAsync(new OrganizationUser + Name = "test-collection-1" + }; + var collection2 = new Collection { OrganizationId = organization.Id, - UserId = user3.Id, - Status = OrganizationUserStatusType.Confirmed, - }); + Name = "test-collection-2" + }; + + await collectionRepository.CreateAsync( + collection1, + groups: [new CollectionAccessSelection { Id = group1.Id, HidePasswords = false, ReadOnly = false, Manage = true }], + users: [new CollectionAccessSelection { Id = orgUser1.Id, HidePasswords = false, ReadOnly = false, Manage = true }]); + await collectionRepository.CreateAsync(collection2, + groups: [new CollectionAccessSelection { Id = group2.Id, HidePasswords = false, ReadOnly = false, Manage = true }], + users: [new CollectionAccessSelection { Id = orgUser3.Id, HidePasswords = false, ReadOnly = false, Manage = true }]); await userRepository.DeleteManyAsync(new List { @@ -94,6 +83,100 @@ public class UserRepositoryTests Assert.Null(orgUser1Deleted); Assert.NotNull(notDeletedOrgUsers); Assert.True(notDeletedOrgUsers.Count > 0); + + var collection1WithUsers = await collectionRepository.GetByIdWithPermissionsAsync(collection1.Id, null, true); + var collection2WithUsers = await collectionRepository.GetByIdWithPermissionsAsync(collection2.Id, null, true); + Assert.Empty(collection1WithUsers.Users); // Collection1 should have no users (orgUser1 was deleted) + Assert.Single(collection2WithUsers.Users); // Collection2 should still have orgUser3 (not deleted) + Assert.Single(collection2WithUsers.Users); + Assert.Equal(orgUser3.Id, collection2WithUsers.Users.First().Id); + + var group1Users = await groupRepository.GetManyUserIdsByIdAsync(group1.Id); + var group2Users = await groupRepository.GetManyUserIdsByIdAsync(group2.Id); + + Assert.Empty(group1Users); // Group1 should have no users (orgUser1 was deleted) + Assert.Single(group2Users); // Group2 should still have orgUser3 (not deleted) + Assert.Equal(orgUser3.Id, group2Users.First()); } + [Theory, DatabaseData] + public async Task DeleteAsync_WhenUserHasDefaultUserCollections_MigratesToSharedCollection( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + var defaultUserCollection = new Collection + { + Name = "Test Collection", + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }; + await collectionRepository.CreateAsync( + defaultUserCollection, + groups: null, + users: [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]); + + await userRepository.DeleteAsync(user); + + var deletedUser = await userRepository.GetByIdAsync(user.Id); + Assert.Null(deletedUser); + + var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id); + Assert.NotNull(updatedCollection); + Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type); + Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail); + } + + [Theory, DatabaseData] + public async Task DeleteManyAsync_WhenUsersHaveDefaultUserCollections_MigratesToSharedCollection( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user1 = await userRepository.CreateTestUserAsync(); + var user2 = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var defaultUserCollection1 = new Collection + { + Name = "Test Collection 1", + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }; + + var defaultUserCollection2 = new Collection + { + Name = "Test Collection 2", + Type = CollectionType.DefaultUserCollection, + OrganizationId = organization.Id + }; + + await collectionRepository.CreateAsync(defaultUserCollection1, groups: null, users: [new CollectionAccessSelection { Id = orgUser1.Id, HidePasswords = false, ReadOnly = false, Manage = true }]); + await collectionRepository.CreateAsync(defaultUserCollection2, groups: null, users: [new CollectionAccessSelection { Id = orgUser2.Id, HidePasswords = false, ReadOnly = false, Manage = true }]); + + await userRepository.DeleteManyAsync([user1, user2]); + + var deletedUser1 = await userRepository.GetByIdAsync(user1.Id); + var deletedUser2 = await userRepository.GetByIdAsync(user2.Id); + Assert.Null(deletedUser1); + Assert.Null(deletedUser2); + + var updatedCollection1 = await collectionRepository.GetByIdAsync(defaultUserCollection1.Id); + Assert.NotNull(updatedCollection1); + Assert.Equal(CollectionType.SharedCollection, updatedCollection1.Type); + Assert.Equal(user1.Email, updatedCollection1.DefaultUserCollectionEmail); + + var updatedCollection2 = await collectionRepository.GetByIdAsync(defaultUserCollection2.Id); + Assert.NotNull(updatedCollection2); + Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type); + Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail); + } } diff --git a/util/Migrator/DbScripts/2025-09-23_00_MigrateDefaultCollectionsOnUserDelete.sql b/util/Migrator/DbScripts/2025-09-23_00_MigrateDefaultCollectionsOnUserDelete.sql new file mode 100644 index 0000000000..517ef732a0 --- /dev/null +++ b/util/Migrator/DbScripts/2025-09-23_00_MigrateDefaultCollectionsOnUserDelete.sql @@ -0,0 +1,325 @@ +CREATE OR ALTER PROCEDURE [dbo].[User_DeleteById] + @Id UNIQUEIDENTIFIER +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + DECLARE @BatchSize INT = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION User_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] = @Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION User_DeleteById_Ciphers + END + + BEGIN TRANSACTION User_DeleteById + + -- Delete WebAuthnCredentials + DELETE + FROM + [dbo].[WebAuthnCredential] + WHERE + [UserId] = @Id + + -- Delete folders + DELETE + FROM + [dbo].[Folder] + WHERE + [UserId] = @Id + + -- Delete AuthRequest, must be before Device + DELETE + FROM + [dbo].[AuthRequest] + WHERE + [UserId] = @Id + + -- Delete devices + DELETE + FROM + [dbo].[Device] + WHERE + [UserId] = @Id + + -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records + DECLARE @OrgUserIds [dbo].[GuidIdArray] + INSERT INTO @OrgUserIds (Id) + SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] = @Id + + IF EXISTS (SELECT 1 FROM @OrgUserIds) + BEGIN + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds + END + + -- Delete collection users + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE + OU.[UserId] = @Id + + -- Delete group users + DELETE + GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE + OU.[UserId] = @Id + + -- Delete AccessPolicy + DELETE + AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId] + WHERE + [UserId] = @Id + + -- Delete organization users + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [UserId] = @Id + + -- Delete provider users + DELETE + FROM + [dbo].[ProviderUser] + WHERE + [UserId] = @Id + + -- Delete SSO Users + DELETE + FROM + [dbo].[SsoUser] + WHERE + [UserId] = @Id + + -- Delete Emergency Accesses + DELETE + FROM + [dbo].[EmergencyAccess] + WHERE + [GrantorId] = @Id + OR + [GranteeId] = @Id + + -- Delete Sends + DELETE + FROM + [dbo].[Send] + WHERE + [UserId] = @Id + + -- Delete Notification Status + DELETE + FROM + [dbo].[NotificationStatus] + WHERE + [UserId] = @Id + + -- Delete Notification + DELETE + FROM + [dbo].[Notification] + WHERE + [UserId] = @Id + + -- Finally, delete the user + DELETE + FROM + [dbo].[User] + WHERE + [Id] = @Id + + COMMIT TRANSACTION User_DeleteById +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[User_DeleteByIds] + @Ids NVARCHAR(MAX) +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON + -- Declare a table variable to hold the parsed JSON data + DECLARE @ParsedIds TABLE (Id UNIQUEIDENTIFIER); + + -- Parse the JSON input into the table variable + INSERT INTO @ParsedIds (Id) + SELECT value + FROM OPENJSON(@Ids); + + -- Check if the input table is empty + IF (SELECT COUNT(1) FROM @ParsedIds) < 1 + BEGIN + RETURN(-1); + END + + DECLARE @BatchSize INT = 100 + + -- Delete ciphers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION User_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION User_DeleteById_Ciphers + END + + BEGIN TRANSACTION User_DeleteById + + -- Delete WebAuthnCredentials + DELETE + FROM + [dbo].[WebAuthnCredential] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete folders + DELETE + FROM + [dbo].[Folder] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AuthRequest, must be before Device + DELETE + FROM + [dbo].[AuthRequest] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete devices + DELETE + FROM + [dbo].[Device] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Migrate DefaultUserCollection to SharedCollection before deleting CollectionUser records + DECLARE @OrgUserIds [dbo].[GuidIdArray] + INSERT INTO @OrgUserIds (Id) + SELECT [Id] FROM [dbo].[OrganizationUser] WHERE [UserId] IN (SELECT * FROM @ParsedIds) + + IF EXISTS (SELECT 1 FROM @OrgUserIds) + BEGIN + EXEC [dbo].[OrganizationUser_MigrateDefaultCollection] @OrgUserIds + END + + -- Delete collection users + DELETE + CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete group users + DELETE + GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId] + WHERE + OU.[UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete AccessPolicy + DELETE + AP + FROM + [dbo].[AccessPolicy] AP + INNER JOIN + [dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete organization users + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete provider users + DELETE + FROM + [dbo].[ProviderUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete SSO Users + DELETE + FROM + [dbo].[SsoUser] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Emergency Accesses + DELETE + FROM + [dbo].[EmergencyAccess] + WHERE + [GrantorId] IN (SELECT * FROM @ParsedIds) + OR + [GranteeId] IN (SELECT * FROM @ParsedIds) + + -- Delete Sends + DELETE + FROM + [dbo].[Send] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification Status + DELETE + FROM + [dbo].[NotificationStatus] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Delete Notification + DELETE + FROM + [dbo].[Notification] + WHERE + [UserId] IN (SELECT * FROM @ParsedIds) + + -- Finally, delete the user + DELETE + FROM + [dbo].[User] + WHERE + [Id] IN (SELECT * FROM @ParsedIds) + + COMMIT TRANSACTION User_DeleteById +END +GO From 61265c7533711591d74a3b0c88364ed9e067df47 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 1 Oct 2025 10:26:39 -0500 Subject: [PATCH 300/326] [PM-25463] Work towards complete usage of Payments domain (#6363) * Use payment domain * Run dotnet format and remove unused code * Fix swagger * Stephon's feedback * Run dotnet format --- .../AdminConsole/Services/ProviderService.cs | 20 +- .../Services/ProviderBillingService.cs | 246 ++-- .../Services/ProviderServiceTests.cs | 83 +- .../Services/ProviderBillingServiceTests.cs | 342 ++--- .../Controllers/ProvidersController.cs | 17 +- .../Providers/ProviderSetupRequestModel.cs | 8 +- .../OrganizationBillingController.cs | 79 +- src/Api/Billing/Controllers/TaxController.cs | 78 +- .../VNext/AccountBillingVNextController.cs | 3 +- .../OrganizationBillingVNextController.cs | 18 + ...ganizationSubscriptionPlanChangeRequest.cs | 31 + ...OrganizationSubscriptionPurchaseRequest.cs | 84 ++ .../OrganizationSubscriptionUpdateRequest.cs | 48 + .../Requests/Payment/BillingAddressRequest.cs | 3 +- .../Requests/Payment/BitPayCreditRequest.cs | 3 +- .../Payment/CheckoutBillingAddressRequest.cs | 3 +- .../Payment/MinimalBillingAddressRequest.cs | 3 +- .../MinimalTokenizedPaymentMethodRequest.cs | 14 +- .../Payment/TokenizedPaymentMethodRequest.cs | 24 +- .../PremiumCloudHostedSubscriptionRequest.cs | 3 +- ...axAmountForOrganizationTrialRequestBody.cs | 27 - .../RestartSubscriptionRequest.cs | 16 + ...izationSubscriptionPlanChangeTaxRequest.cs | 19 + ...anizationSubscriptionPurchaseTaxRequest.cs | 19 + ...rganizationSubscriptionUpdateTaxRequest.cs | 11 + ...ewPremiumSubscriptionPurchaseTaxRequest.cs | 17 + .../AdminConsole/Services/IProviderService.cs | 5 +- .../NoopProviderService.cs | 4 +- .../Billing/Commands/BillingCommandResult.cs | 29 +- src/Core/Billing/Enums/PlanCadenceType.cs | 7 + .../Extensions/ServiceCollectionExtensions.cs | 6 +- .../Commands/PreviewOrganizationTaxCommand.cs | 383 +++++ .../OrganizationSubscriptionPlanChange.cs | 23 + .../OrganizationSubscriptionPurchase.cs | 39 + .../Models/OrganizationSubscriptionUpdate.cs | 19 + .../Commands/PreviewPremiumTaxCommand.cs | 65 + .../Implementations/ProviderMigrator.cs | 2 +- .../Services/IProviderBillingService.cs | 12 +- .../Commands/RestartSubscriptionCommand.cs | 92 ++ .../Tax/Commands/PreviewTaxAmountCommand.cs | 136 -- src/Core/Constants.cs | 1 - .../Services/Implementations/StripeAdapter.cs | 3 + .../PreviewOrganizationTaxCommandTests.cs | 1262 +++++++++++++++++ .../Commands/PreviewPremiumTaxCommandTests.cs | 292 ++++ .../RestartSubscriptionCommandTests.cs | 198 +++ .../Commands/PreviewTaxAmountCommandTests.cs | 541 ------- 46 files changed, 2988 insertions(+), 1350 deletions(-) create mode 100644 src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPlanChangeRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPurchaseRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionUpdateRequest.cs delete mode 100644 src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs create mode 100644 src/Api/Billing/Models/Requests/Subscriptions/RestartSubscriptionRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPlanChangeTaxRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPurchaseTaxRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionUpdateTaxRequest.cs create mode 100644 src/Api/Billing/Models/Requests/Tax/PreviewPremiumSubscriptionPurchaseTaxRequest.cs create mode 100644 src/Core/Billing/Enums/PlanCadenceType.cs create mode 100644 src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs create mode 100644 src/Core/Billing/Organizations/Models/OrganizationSubscriptionPlanChange.cs create mode 100644 src/Core/Billing/Organizations/Models/OrganizationSubscriptionPurchase.cs create mode 100644 src/Core/Billing/Organizations/Models/OrganizationSubscriptionUpdate.cs create mode 100644 src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs create mode 100644 src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs delete mode 100644 src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs create mode 100644 test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs create mode 100644 test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs create mode 100644 test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs delete mode 100644 test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index aa19ad5382..aaf0050b63 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -12,7 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; @@ -90,7 +90,7 @@ public class ProviderService : IProviderService _providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand; } - public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) + public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) { var owner = await _userService.GetUserByIdAsync(ownerUserId); if (owner == null) @@ -115,21 +115,7 @@ public class ProviderService : IProviderService throw new BadRequestException("Invalid owner."); } - if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode)) - { - throw new BadRequestException("Both address and postal code are required to set up your provider."); - } - - if (tokenizedPaymentSource is not - { - Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, - Token: not null and not "" - }) - { - throw new BadRequestException("A payment method is required to set up your provider."); - } - - var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress); provider.GatewayCustomerId = customer.Id; var subscription = await _providerBillingService.SetupSubscription(provider); provider.GatewaySubscriptionId = subscription.Id; 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 398674c7b6..c9851eb403 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -14,6 +14,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Models; @@ -21,10 +22,8 @@ using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -38,6 +37,9 @@ using Subscription = Stripe.Subscription; namespace Bit.Commercial.Core.Billing.Providers.Services; +using static Constants; +using static StripeConstants; + public class ProviderBillingService( IBraintreeGateway braintreeGateway, IEventService eventService, @@ -51,8 +53,7 @@ public class ProviderBillingService( IProviderUserRepository providerUserRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService, - ITaxService taxService) + ISubscriberService subscriberService) : IProviderBillingService { public async Task AddExistingOrganization( @@ -61,10 +62,7 @@ public class ProviderBillingService( string key) { await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, - new SubscriptionUpdateOptions - { - CancelAtPeriodEnd = false - }); + new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }); var subscription = await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId, @@ -83,7 +81,7 @@ public class ProviderBillingService( var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now; - if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft) + if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft) { await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId, new InvoiceFinalizeOptions { AutoAdvance = true }); @@ -184,16 +182,8 @@ public class ProviderBillingService( { Items = [ - new SubscriptionItemOptions - { - Price = newPriceId, - Quantity = oldSubscriptionItem!.Quantity - }, - new SubscriptionItemOptions - { - Id = oldSubscriptionItem.Id, - Deleted = true - } + new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity }, + new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true } ] }; @@ -202,7 +192,8 @@ public class ProviderBillingService( // Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId) // 1. Retrieve PlanType and PlanName for ProviderPlan // 2. Assign PlanType & PlanName to Organization - var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId); + var providerOrganizations = + await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId); var newPlan = await pricingClient.GetPlanOrThrow(newPlanType); @@ -213,6 +204,7 @@ public class ProviderBillingService( { throw new ConflictException($"Organization '{providerOrganization.Id}' not found."); } + organization.PlanType = newPlanType; organization.Plan = newPlan.Name; await organizationRepository.ReplaceAsync(organization); @@ -228,15 +220,15 @@ public class ProviderBillingService( if (!string.IsNullOrEmpty(organization.GatewayCustomerId)) { - logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, nameof(organization.GatewayCustomerId)); + logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, + nameof(organization.GatewayCustomerId)); return; } - var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions - { - Expand = ["tax", "tax_ids"] - }); + var providerCustomer = + await subscriberService.GetCustomerOrThrow(provider, + new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); var providerTaxId = providerCustomer.TaxIds.FirstOrDefault(); @@ -269,23 +261,18 @@ public class ProviderBillingService( } ] }, - Metadata = new Dictionary - { - { "region", globalSettings.BaseServiceUri.CloudRegion } - }, - TaxIdData = providerTaxId == null ? null : - [ - new CustomerTaxIdDataOptions - { - Type = providerTaxId.Type, - Value = providerTaxId.Value - } - ] + Metadata = new Dictionary { { "region", globalSettings.BaseServiceUri.CloudRegion } }, + TaxIdData = providerTaxId == null + ? null + : + [ + new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value } + ] }; - if (providerCustomer.Address is not { Country: Constants.CountryAbbreviations.UnitedStates }) + if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates }) { - customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; + customerCreateOptions.TaxExempt = TaxExempt.Reverse; } var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions); @@ -347,9 +334,9 @@ public class ProviderBillingService( .Where(pair => pair.subscription is { Status: - StripeConstants.SubscriptionStatus.Active or - StripeConstants.SubscriptionStatus.Trialing or - StripeConstants.SubscriptionStatus.PastDue + SubscriptionStatus.Active or + SubscriptionStatus.Trialing or + SubscriptionStatus.PastDue }).ToList(); if (active.Count == 0) @@ -474,37 +461,27 @@ public class ProviderBillingService( // Below the limit to above the limit (currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) || // Above the limit to further above the limit - (currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal); + (currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && + newlyAssignedSeatTotal > currentlyAssignedSeatTotal); } public async Task SetupCustomer( Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource) + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress) { - ArgumentNullException.ThrowIfNull(tokenizedPaymentSource); - - if (taxInfo is not - { - BillingAddressCountry: not null and not "", - BillingAddressPostalCode: not null and not "" - }) - { - logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id); - throw new BillingException(); - } - var options = new CustomerCreateOptions { Address = new AddressOptions { - Country = taxInfo.BillingAddressCountry, - PostalCode = taxInfo.BillingAddressPostalCode, - Line1 = taxInfo.BillingAddressLine1, - Line2 = taxInfo.BillingAddressLine2, - City = taxInfo.BillingAddressCity, - State = taxInfo.BillingAddressState + Country = billingAddress.Country, + PostalCode = billingAddress.PostalCode, + Line1 = billingAddress.Line1, + Line2 = billingAddress.Line2, + City = billingAddress.City, + State = billingAddress.State }, + Coupon = !string.IsNullOrEmpty(provider.DiscountId) ? provider.DiscountId : null, Description = provider.DisplayBusinessName(), Email = provider.BillingEmail, InvoiceSettings = new CustomerInvoiceSettingsOptions @@ -520,93 +497,61 @@ public class ProviderBillingService( } ] }, - Metadata = new Dictionary - { - { "region", globalSettings.BaseServiceUri.CloudRegion } - } + Metadata = new Dictionary { { "region", globalSettings.BaseServiceUri.CloudRegion } }, + TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None }; - if (taxInfo.BillingAddressCountry is not Constants.CountryAbbreviations.UnitedStates) + if (billingAddress.TaxId != null) { - options.TaxExempt = StripeConstants.TaxExempt.Reverse; - } - - if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber)) - { - var taxIdType = taxService.GetStripeTaxCode( - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - - if (taxIdType == null) - { - logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - options.TaxIdData = [ - new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber } + new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value } ]; - if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) + if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF) { options.TaxIdData.Add(new CustomerTaxIdDataOptions { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{taxInfo.TaxIdNumber}" + Type = TaxIdType.EUVAT, + Value = $"ES{billingAddress.TaxId.Value}" }); } } - if (!string.IsNullOrEmpty(provider.DiscountId)) - { - options.Coupon = provider.DiscountId; - } - var braintreeCustomerId = ""; - if (tokenizedPaymentSource is not - { - Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, - Token: not null and not "" - }) - { - logger.LogError("Cannot create customer for provider ({ProviderID}) with invalid payment method", provider.Id); - throw new BillingException(); - } - - var (type, token) = tokenizedPaymentSource; - // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (type) + switch (paymentMethod.Type) { - case PaymentMethodType.BankAccount: + case TokenizablePaymentMethodType.BankAccount: { var setupIntent = - (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token })) + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions + { + PaymentMethod = paymentMethod.Token + })) .FirstOrDefault(); if (setupIntent == null) { - logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id); + logger.LogError( + "Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", + provider.Id); throw new BillingException(); } await setupIntentCache.Set(provider.Id, setupIntent.Id); break; } - case PaymentMethodType.Card: + case TokenizablePaymentMethodType.Card: { - options.PaymentMethod = token; - options.InvoiceSettings.DefaultPaymentMethod = token; + options.PaymentMethod = paymentMethod.Token; + options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token; break; } - case PaymentMethodType.PayPal: + case TokenizablePaymentMethodType.PayPal: { - braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token); + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token); options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; break; } @@ -616,8 +561,7 @@ public class ProviderBillingService( { return await stripeAdapter.CustomerCreateAsync(options); } - catch (StripeException stripeException) when (stripeException.StripeError?.Code == - StripeConstants.ErrorCodes.TaxIdInvalid) + catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid) { await Revert(); throw new BadRequestException( @@ -632,9 +576,9 @@ public class ProviderBillingService( async Task Revert() { // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault - switch (tokenizedPaymentSource.Type) + switch (paymentMethod.Type) { - case PaymentMethodType.BankAccount: + case TokenizablePaymentMethodType.BankAccount: { var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id); await stripeAdapter.SetupIntentCancel(setupIntentId, @@ -642,7 +586,7 @@ public class ProviderBillingService( await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id); break; } - case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): { await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); break; @@ -661,9 +605,10 @@ public class ProviderBillingService( var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); - if (providerPlans == null || providerPlans.Count == 0) + if (providerPlans.Count == 0) { - logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", provider.Id); + logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", + provider.Id); throw new BillingException(); } @@ -676,7 +621,9 @@ public class ProviderBillingService( if (!providerPlan.IsConfigured()) { - logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name); + logger.LogError( + "Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", + provider.Id, plan.Name); throw new BillingException(); } @@ -692,16 +639,14 @@ public class ProviderBillingService( var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id); var setupIntent = !string.IsNullOrEmpty(setupIntentId) - ? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions - { - Expand = ["payment_method"] - }) + ? await stripeAdapter.SetupIntentGet(setupIntentId, + new SetupIntentGetOptions { Expand = ["payment_method"] }) : null; var usePaymentMethod = !string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) || - (customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true) || - (setupIntent?.IsUnverifiedBankAccount() == true); + customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true || + setupIntent?.IsUnverifiedBankAccount() == true; int? trialPeriodDays = provider.Type switch { @@ -712,30 +657,28 @@ public class ProviderBillingService( var subscriptionCreateOptions = new SubscriptionCreateOptions { - CollectionMethod = usePaymentMethod ? - StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice, + CollectionMethod = + usePaymentMethod + ? CollectionMethod.ChargeAutomatically + : CollectionMethod.SendInvoice, Customer = customer.Id, DaysUntilDue = usePaymentMethod ? null : 30, Items = subscriptionItemOptionsList, - Metadata = new Dictionary - { - { "providerId", provider.Id.ToString() } - }, + Metadata = new Dictionary { { "providerId", provider.Id.ToString() } }, OffSession = true, - ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, - TrialPeriodDays = trialPeriodDays + ProrationBehavior = ProrationBehavior.CreateProrations, + TrialPeriodDays = trialPeriodDays, + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }; - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - try { var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); if (subscription is { - Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing + Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing }) { return subscription; @@ -749,9 +692,11 @@ public class ProviderBillingService( throw new BillingException(); } - catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) + catch (StripeException stripeException) when (stripeException.StripeError?.Code == + ErrorCodes.CustomerTaxLocationInvalid) { - throw new BadRequestException("Your location wasn't recognized. Please ensure your country and postal code are valid."); + throw new BadRequestException( + "Your location wasn't recognized. Please ensure your country and postal code are valid."); } } @@ -765,7 +710,7 @@ public class ProviderBillingService( subscriberService.UpdateTaxInformation(provider, taxInformation)); await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, - new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically }); + new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically }); } public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) @@ -865,13 +810,9 @@ public class ProviderBillingService( await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions { - Items = [ - new SubscriptionItemOptions - { - Id = item.Id, - Price = priceId, - Quantity = newlySubscribedSeats - } + Items = + [ + new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats } ] }); @@ -894,7 +835,8 @@ public class ProviderBillingService( var plan = await pricingClient.GetPlanOrThrow(planType); return providerOrganizations - .Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed) + .Where(providerOrganization => providerOrganization.Plan == plan.Name && + providerOrganization.Status == OrganizationStatusType.Managed) .Sum(providerOrganization => providerOrganization.Seats ?? 0); } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index f2ba2fab8f..e61cf5f97e 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -9,7 +9,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; @@ -41,7 +41,7 @@ public class ProviderServiceTests public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider sutProvider) { var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null)); + () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null)); Assert.Contains("Invalid owner.", exception.Message); } @@ -53,83 +53,12 @@ public class ProviderServiceTests userService.GetUserByIdAsync(user.Id).Returns(user); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null)); + () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null)); Assert.Contains("Invalid token.", exception.Message); } [Theory, BitAutoData] - public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException( - User user, - Provider provider, - string key, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource, - [ProviderUser] ProviderUser providerUser, - SutProvider sutProvider) - { - providerUser.ProviderId = provider.Id; - providerUser.UserId = user.Id; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(user.Id).Returns(user); - - var providerUserRepository = sutProvider.GetDependency(); - providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); - - var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); - var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); - sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") - .Returns(protector); - - sutProvider.Create(); - - var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - - taxInfo.BillingAddressCountry = null; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); - - Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message); - } - - [Theory, BitAutoData] - public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException( - User user, - Provider provider, - string key, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource, - [ProviderUser] ProviderUser providerUser, - SutProvider sutProvider) - { - providerUser.ProviderId = provider.Id; - providerUser.UserId = user.Id; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(user.Id).Returns(user); - - var providerUserRepository = sutProvider.GetDependency(); - providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); - - var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); - var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); - sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") - .Returns(protector); - - sutProvider.Create(); - - var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - - - tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); - - Assert.Equal("A payment method is required to set up your provider.", exception.Message); - } - - [Theory, BitAutoData] - public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource, + public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress, [ProviderUser] ProviderUser providerUser, SutProvider sutProvider) { @@ -149,7 +78,7 @@ public class ProviderServiceTests var providerBillingService = sutProvider.GetDependency(); var customer = new Customer { Id = "customer_id" }; - providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer); + providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer); var subscription = new Subscription { Id = "subscription_id" }; providerBillingService.SetupSubscription(provider).Returns(subscription); @@ -158,7 +87,7 @@ public class ProviderServiceTests var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource); + await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress); await sutProvider.GetDependency().Received().UpsertAsync(Arg.Is( p => diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 54c0b82aa9..18c71364e6 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Net; using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities; @@ -10,18 +9,16 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -895,118 +892,53 @@ public class ProviderBillingServiceTests #region SetupCustomer [Theory, BitAutoData] - public async Task SetupCustomer_MissingCountry_ContactSupport( + public async Task SetupCustomer_NullPaymentMethod_ThrowsNullReferenceException( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource) + BillingAddress billingAddress) { - taxInfo.BillingAddressCountry = null; - - await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .CustomerGetAsync(Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task SetupCustomer_MissingPostalCode_ContactSupport( - SutProvider sutProvider, - Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource) - { - taxInfo.BillingAddressCountry = null; - - await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .CustomerGetAsync(Arg.Any(), Arg.Any()); - } - - - [Theory, BitAutoData] - public async Task SetupCustomer_NullPaymentSource_ThrowsArgumentNullException( - SutProvider sutProvider, - Provider provider, - TaxInfo taxInfo) - { - await Assert.ThrowsAsync(() => - sutProvider.Sut.SetupCustomer(provider, taxInfo, null)); - } - - [Theory, BitAutoData] - public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException( - SutProvider sutProvider, - Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource) - { - provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; - - - tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; - - await ThrowsBillingExceptionAsync(() => - sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, null, billingAddress)); } [Theory, BitAutoData] public async Task SetupCustomer_WithBankAccount_Error_Reverts( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "12345678Z"); var stripeAdapter = sutProvider.GetDependency(); - - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); - + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" }; stripeAdapter.SetupIntentList(Arg.Is(options => - options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([ new SetupIntent { Id = "setup_intent_id" } ]); stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value)) .Throws(); sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id"); await Assert.ThrowsAsync(() => - sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress)); await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_id"); @@ -1020,45 +952,37 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_WithPayPal_Error_Reverts( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "12345678Z"); var stripeAdapter = sutProvider.GetDependency(); + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" }; - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); - - - sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token) .Returns("braintree_customer_id"); stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && o.Metadata["btCustomerId"] == "braintree_customer_id" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value)) .Throws(); await Assert.ThrowsAsync(() => - sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress)); await sutProvider.GetDependency().Customer.Received(1).DeleteAsync("braintree_customer_id"); } @@ -1067,17 +991,11 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_WithBankAccount_Success( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "12345678Z"); var stripeAdapter = sutProvider.GetDependency(); @@ -1087,31 +1005,30 @@ public class ProviderBillingServiceTests Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); - + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" }; stripeAdapter.SetupIntentList(Arg.Is(options => - options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([ new SetupIntent { Id = "setup_intent_id" } ]); stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value)) .Returns(expected); - var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress); Assert.Equivalent(expected, actual); @@ -1122,17 +1039,11 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_WithPayPal_Success( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "12345678Z"); var stripeAdapter = sutProvider.GetDependency(); @@ -1142,30 +1053,29 @@ public class ProviderBillingServiceTests Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" }; - - sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token) .Returns("braintree_customer_id"); stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && o.Metadata["btCustomerId"] == "braintree_customer_id" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value)) .Returns(expected); - var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress); Assert.Equivalent(expected, actual); } @@ -1174,17 +1084,11 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_WithCard_Success( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "12345678Z"); var stripeAdapter = sutProvider.GetDependency(); @@ -1194,28 +1098,26 @@ public class ProviderBillingServiceTests Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); - + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" }; stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.PaymentMethod == tokenizedPaymentSource.Token && - o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value)) .Returns(expected); - var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress); Assert.Equivalent(expected, actual); } @@ -1224,17 +1126,11 @@ public class ProviderBillingServiceTests public async Task SetupCustomer_WithCard_ReverseCharge_Success( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo) + BillingAddress billingAddress) { provider.Name = "MSP"; - - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns(taxInfo.TaxIdType); - - taxInfo.BillingAddressCountry = "AD"; + billingAddress.Country = "FR"; // Non-US country to trigger reverse charge + billingAddress.TaxId = new TaxID("fr_siren", "123456789"); var stripeAdapter = sutProvider.GetDependency(); @@ -1244,55 +1140,51 @@ public class ProviderBillingServiceTests Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } }; - var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); - + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" }; stripeAdapter.CustomerCreateAsync(Arg.Is(o => - o.Address.Country == taxInfo.BillingAddressCountry && - o.Address.PostalCode == taxInfo.BillingAddressPostalCode && - o.Address.Line1 == taxInfo.BillingAddressLine1 && - o.Address.Line2 == taxInfo.BillingAddressLine2 && - o.Address.City == taxInfo.BillingAddressCity && - o.Address.State == taxInfo.BillingAddressState && - o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Address.Country == billingAddress.Country && + o.Address.PostalCode == billingAddress.PostalCode && + o.Address.Line1 == billingAddress.Line1 && + o.Address.Line2 == billingAddress.Line2 && + o.Address.City == billingAddress.City && + o.Address.State == billingAddress.State && + o.Description == provider.DisplayBusinessName() && o.Email == provider.BillingEmail && - o.PaymentMethod == tokenizedPaymentSource.Token && - o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && - o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber && + o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && + o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value && o.TaxExempt == StripeConstants.TaxExempt.Reverse)) .Returns(expected); - var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress); Assert.Equivalent(expected, actual); } [Theory, BitAutoData] - public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( + public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException( SutProvider sutProvider, Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource) + BillingAddress billingAddress) { provider.Name = "MSP"; + billingAddress.Country = "AD"; + billingAddress.TaxId = new TaxID("es_nif", "invalid_tax_id"); - taxInfo.BillingAddressCountry = "AD"; + var stripeAdapter = sutProvider.GetDependency(); + var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" }; - sutProvider.GetDependency() - .GetStripeTaxCode(Arg.Is( - p => p == taxInfo.BillingAddressCountry), - Arg.Is(p => p == taxInfo.TaxIdNumber)) - .Returns((string)null); + stripeAdapter.CustomerCreateAsync(Arg.Any()) + .Throws(new StripeException("Invalid tax ID") { StripeError = new StripeError { Code = "tax_id_invalid" } }); var actual = await Assert.ThrowsAsync(async () => - await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress)); - Assert.IsType(actual); - Assert.Equal("billingTaxIdTypeInferenceError", actual.Message); + Assert.Equal("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.", actual.Message); } #endregion diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs index a1815fd3bf..aa87bf9c74 100644 --- a/src/Api/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs @@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Context; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.AspNetCore.Authorization; @@ -93,22 +92,12 @@ public class ProvidersController : Controller var userId = _userService.GetProperUserId(User).Value; - var taxInfo = new TaxInfo - { - BillingAddressCountry = model.TaxInfo.Country, - BillingAddressPostalCode = model.TaxInfo.PostalCode, - TaxIdNumber = model.TaxInfo.TaxId, - BillingAddressLine1 = model.TaxInfo.Line1, - BillingAddressLine2 = model.TaxInfo.Line2, - BillingAddressCity = model.TaxInfo.City, - BillingAddressState = model.TaxInfo.State - }; - - var tokenizedPaymentSource = model.PaymentSource?.ToDomain(); + var paymentMethod = model.PaymentMethod.ToDomain(); + var billingAddress = model.BillingAddress.ToDomain(); var response = await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key, - taxInfo, tokenizedPaymentSource); + paymentMethod, billingAddress); return new ProviderResponseModel(response); } diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs index 1f50c384a3..41cebe8b9b 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs @@ -3,8 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using Bit.Api.Billing.Models.Requests; -using Bit.Api.Models.Request; +using Bit.Api.Billing.Models.Requests.Payment; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Utilities; @@ -28,8 +27,9 @@ public class ProviderSetupRequestModel [Required] public string Key { get; set; } [Required] - public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; } - public TokenizedPaymentSourceRequestBody PaymentSource { get; set; } + public MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; } + [Required] + public BillingAddressRequest BillingAddress { get; set; } public virtual Provider ToProvider(Provider provider) { diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 21b17bff67..1d6bf51661 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,16 +1,8 @@ -#nullable enable -using System.Diagnostics; -using Bit.Api.AdminConsole.Models.Request.Organizations; -using Bit.Api.Billing.Models.Requests; +using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; -using Bit.Core.Billing.Organizations.Models; using Bit.Core.Billing.Organizations.Services; -using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; @@ -28,10 +20,8 @@ public class OrganizationBillingController( IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, IPaymentService paymentService, - IPricingClient pricingClient, ISubscriberService subscriberService, - IPaymentHistoryService paymentHistoryService, - IUserService userService) : BaseBillingController + IPaymentHistoryService paymentHistoryService) : BaseBillingController { [HttpGet("metadata")] public async Task GetMetadataAsync([FromRoute] Guid organizationId) @@ -264,71 +254,6 @@ public class OrganizationBillingController( return TypedResults.Ok(); } - [HttpPost("restart-subscription")] - public async Task RestartSubscriptionAsync([FromRoute] Guid organizationId, - [FromBody] OrganizationCreateRequestModel model) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - if (!await currentContext.EditPaymentMethods(organizationId)) - { - return Error.Unauthorized(); - } - - var organization = await organizationRepository.GetByIdAsync(organizationId); - if (organization == null) - { - return Error.NotFound(); - } - var existingPlan = organization.PlanType; - var organizationSignup = model.ToOrganizationSignup(user); - var sale = OrganizationSale.From(organization, organizationSignup); - var plan = await pricingClient.GetPlanOrThrow(model.PlanType); - sale.Organization.PlanType = plan.Type; - sale.Organization.Plan = plan.Name; - sale.SubscriptionSetup.SkipTrial = true; - if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null) - { - sale.Organization.UseTotp = plan.HasTotp; - sale.Organization.UseGroups = plan.HasGroups; - sale.Organization.UseDirectory = plan.HasDirectory; - sale.Organization.SelfHost = plan.HasSelfHost; - sale.Organization.UsersGetPremium = plan.UsersGetPremium; - sale.Organization.UseEvents = plan.HasEvents; - sale.Organization.Use2fa = plan.Has2fa; - sale.Organization.UseApi = plan.HasApi; - sale.Organization.UsePolicies = plan.HasPolicies; - sale.Organization.UseSso = plan.HasSso; - sale.Organization.UseResetPassword = plan.HasResetPassword; - sale.Organization.UseKeyConnector = plan.HasKeyConnector ? organization.UseKeyConnector : false; - sale.Organization.UseScim = plan.HasScim; - sale.Organization.UseCustomPermissions = plan.HasCustomPermissions; - sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains; - sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections; - } - - if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken)) - { - return Error.BadRequest("A payment method is required to restart the subscription."); - } - var org = await organizationRepository.GetByIdAsync(organizationId); - Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); - var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); - var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); - await organizationBillingService.Finalize(sale); - var updatedOrg = await organizationRepository.GetByIdAsync(organizationId); - if (updatedOrg != null) - { - await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation); - } - - return TypedResults.Ok(); - } - [HttpPost("setup-business-unit")] [SelfHosted(NotSelfHostedOnly = true)] public async Task SetupBusinessUnitAsync( diff --git a/src/Api/Billing/Controllers/TaxController.cs b/src/Api/Billing/Controllers/TaxController.cs index d2c1c36726..4ead414589 100644 --- a/src/Api/Billing/Controllers/TaxController.cs +++ b/src/Api/Billing/Controllers/TaxController.cs @@ -1,33 +1,73 @@ -using Bit.Api.Billing.Models.Requests; -using Bit.Core.Billing.Tax.Commands; +using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Models.Requests.Tax; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Organizations.Commands; +using Bit.Core.Billing.Premium.Commands; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Api.Billing.Controllers; [Authorize("Application")] -[Route("tax")] +[Route("billing/tax")] public class TaxController( - IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController + IPreviewOrganizationTaxCommand previewOrganizationTaxCommand, + IPreviewPremiumTaxCommand previewPremiumTaxCommand) : BaseBillingController { - [HttpPost("preview-amount/organization-trial")] - public async Task PreviewTaxAmountForOrganizationTrialAsync( - [FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody) + [HttpPost("organizations/subscriptions/purchase")] + public async Task PreviewOrganizationSubscriptionPurchaseTaxAsync( + [FromBody] PreviewOrganizationSubscriptionPurchaseTaxRequest request) { - var parameters = new OrganizationTrialParameters + var (purchase, billingAddress) = request.ToDomain(); + var result = await previewOrganizationTaxCommand.Run(purchase, billingAddress); + return Handle(result.Map(pair => new { - PlanType = requestBody.PlanType, - ProductType = requestBody.ProductType, - TaxInformation = new OrganizationTrialParameters.TaxInformationDTO - { - Country = requestBody.TaxInformation.Country, - PostalCode = requestBody.TaxInformation.PostalCode, - TaxId = requestBody.TaxInformation.TaxId - } - }; + pair.Tax, + pair.Total + })); + } - var result = await previewTaxAmountCommand.Run(parameters); + [HttpPost("organizations/{organizationId:guid}/subscription/plan-change")] + [InjectOrganization] + public async Task PreviewOrganizationSubscriptionPlanChangeTaxAsync( + [BindNever] Organization organization, + [FromBody] PreviewOrganizationSubscriptionPlanChangeTaxRequest request) + { + var (planChange, billingAddress) = request.ToDomain(); + var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress); + return Handle(result.Map(pair => new + { + pair.Tax, + pair.Total + })); + } - return Handle(result); + [HttpPut("organizations/{organizationId:guid}/subscription/update")] + [InjectOrganization] + public async Task PreviewOrganizationSubscriptionUpdateTaxAsync( + [BindNever] Organization organization, + [FromBody] PreviewOrganizationSubscriptionUpdateTaxRequest request) + { + var update = request.ToDomain(); + var result = await previewOrganizationTaxCommand.Run(organization, update); + return Handle(result.Map(pair => new + { + pair.Tax, + pair.Total + })); + } + + [HttpPost("premium/subscriptions/purchase")] + public async Task PreviewPremiumSubscriptionPurchaseTaxAsync( + [FromBody] PreviewPremiumSubscriptionPurchaseTaxRequest request) + { + var (purchase, billingAddress) = request.ToDomain(); + var result = await previewPremiumTaxCommand.Run(purchase, billingAddress); + return Handle(result.Map(pair => new + { + pair.Tax, + pair.Total + })); } } diff --git a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs index a996290507..97f2003d29 100644 --- a/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/AccountBillingVNextController.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Api.Billing.Models.Requests.Premium; using Bit.Core; diff --git a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs index ee98031dbc..2f825f2cb9 100644 --- a/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs +++ b/src/Api/Billing/Controllers/VNext/OrganizationBillingVNextController.cs @@ -2,11 +2,14 @@ using Bit.Api.AdminConsole.Authorization.Requirements; 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.AdminConsole.Entities; +using Bit.Core.Billing.Commands; using Bit.Core.Billing.Organizations.Queries; using Bit.Core.Billing.Payment.Commands; using Bit.Core.Billing.Payment.Queries; +using Bit.Core.Billing.Subscriptions.Commands; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -24,6 +27,7 @@ public class OrganizationBillingVNextController( IGetCreditQuery getCreditQuery, IGetOrganizationWarningsQuery getOrganizationWarningsQuery, IGetPaymentMethodQuery getPaymentMethodQuery, + IRestartSubscriptionCommand restartSubscriptionCommand, IUpdateBillingAddressCommand updateBillingAddressCommand, IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController { @@ -95,6 +99,20 @@ public class OrganizationBillingVNextController( return Handle(result); } + [Authorize] + [HttpPost("subscription/restart")] + [InjectOrganization] + public async Task RestartSubscriptionAsync( + [BindNever] Organization organization, + [FromBody] RestartSubscriptionRequest request) + { + var (paymentMethod, billingAddress) = request.ToDomain(); + var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, null) + .AndThenAsync(_ => updateBillingAddressCommand.Run(organization, billingAddress)) + .AndThenAsync(_ => restartSubscriptionCommand.Run(organization)); + return Handle(result); + } + [Authorize] [HttpGet("warnings")] [InjectOrganization] diff --git a/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPlanChangeRequest.cs b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPlanChangeRequest.cs new file mode 100644 index 0000000000..a3856bf173 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPlanChangeRequest.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Organizations.Models; + +namespace Bit.Api.Billing.Models.Requests.Organizations; + +public record OrganizationSubscriptionPlanChangeRequest : IValidatableObject +{ + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ProductTierType Tier { get; set; } + + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public PlanCadenceType Cadence { get; set; } + + public OrganizationSubscriptionPlanChange ToDomain() => new() + { + Tier = Tier, + Cadence = Cadence + }; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Tier == ProductTierType.Families && Cadence == PlanCadenceType.Monthly) + { + yield return new ValidationResult("Monthly billing cadence is not available for the Families plan."); + } + } +} diff --git a/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPurchaseRequest.cs b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPurchaseRequest.cs new file mode 100644 index 0000000000..c678b1966c --- /dev/null +++ b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionPurchaseRequest.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Organizations.Models; + +namespace Bit.Api.Billing.Models.Requests.Organizations; + +public record OrganizationSubscriptionPurchaseRequest : IValidatableObject +{ + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ProductTierType Tier { get; set; } + + [Required] + [JsonConverter(typeof(JsonStringEnumConverter))] + public PlanCadenceType Cadence { get; set; } + + [Required] + public required PasswordManagerPurchaseSelections PasswordManager { get; set; } + + public SecretsManagerPurchaseSelections? SecretsManager { get; set; } + + public OrganizationSubscriptionPurchase ToDomain() => new() + { + Tier = Tier, + Cadence = Cadence, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = PasswordManager.Seats, + AdditionalStorage = PasswordManager.AdditionalStorage, + Sponsored = PasswordManager.Sponsored + }, + SecretsManager = SecretsManager != null ? new OrganizationSubscriptionPurchase.SecretsManagerSelections + { + Seats = SecretsManager.Seats, + AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts, + Standalone = SecretsManager.Standalone + } : null + }; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (Tier != ProductTierType.Families) + { + yield break; + } + + if (Cadence == PlanCadenceType.Monthly) + { + yield return new ValidationResult("Monthly cadence is not available on the Families plan."); + } + + if (SecretsManager != null) + { + yield return new ValidationResult("Secrets Manager is not available on the Families plan."); + } + } + + public record PasswordManagerPurchaseSelections + { + [Required] + [Range(1, 100000, ErrorMessage = "Password Manager seats must be between 1 and 100,000")] + public int Seats { get; set; } + + [Required] + [Range(0, 99, ErrorMessage = "Additional storage must be between 0 and 99 GB")] + public int AdditionalStorage { get; set; } + + public bool Sponsored { get; set; } = false; + } + + public record SecretsManagerPurchaseSelections + { + [Required] + [Range(1, 100000, ErrorMessage = "Secrets Manager seats must be between 1 and 100,000")] + public int Seats { get; set; } + + [Required] + [Range(0, 100000, ErrorMessage = "Additional service accounts must be between 0 and 100,000")] + public int AdditionalServiceAccounts { get; set; } + + public bool Standalone { get; set; } = false; + } +} diff --git a/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionUpdateRequest.cs b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionUpdateRequest.cs new file mode 100644 index 0000000000..ad5c3bd609 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Organizations/OrganizationSubscriptionUpdateRequest.cs @@ -0,0 +1,48 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Organizations.Models; + +namespace Bit.Api.Billing.Models.Requests.Organizations; + +public record OrganizationSubscriptionUpdateRequest +{ + public PasswordManagerUpdateSelections? PasswordManager { get; set; } + public SecretsManagerUpdateSelections? SecretsManager { get; set; } + + public OrganizationSubscriptionUpdate ToDomain() => new() + { + PasswordManager = + PasswordManager != null + ? new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = PasswordManager.Seats, + AdditionalStorage = PasswordManager.AdditionalStorage + } + : null, + SecretsManager = + SecretsManager != null + ? new OrganizationSubscriptionUpdate.SecretsManagerSelections + { + Seats = SecretsManager.Seats, + AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts + } + : null + }; + + public record PasswordManagerUpdateSelections + { + [Range(1, 100000, ErrorMessage = "Password Manager seats must be between 1 and 100,000")] + public int? Seats { get; set; } + + [Range(0, 99, ErrorMessage = "Additional storage must be between 0 and 99 GB")] + public int? AdditionalStorage { get; set; } + } + + public record SecretsManagerUpdateSelections + { + [Range(0, 100000, ErrorMessage = "Secrets Manager seats must be between 0 and 100,000")] + public int? Seats { get; set; } + + [Range(0, 100000, ErrorMessage = "Additional service accounts must be between 0 and 100,000")] + public int? AdditionalServiceAccounts { get; set; } + } +} diff --git a/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs index 5c3c47f585..0426a51f10 100644 --- a/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/BillingAddressRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Payment; diff --git a/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs b/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs index bb6e7498d7..ec1405c566 100644 --- a/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/BitPayCreditRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Bit.Api.Billing.Models.Requests.Payment; diff --git a/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs index 54116e897d..ccf2b30b50 100644 --- a/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/CheckoutBillingAddressRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Payment; diff --git a/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs b/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs index b4d28017d5..29c10e6631 100644 --- a/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/MinimalBillingAddressRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Payment; diff --git a/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs index 3b50d2bf63..b0e415c262 100644 --- a/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/MinimalTokenizedPaymentMethodRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Api.Billing.Attributes; using Bit.Core.Billing.Payment.Models; @@ -14,12 +13,9 @@ public class MinimalTokenizedPaymentMethodRequest [Required] public required string Token { get; set; } - public TokenizedPaymentMethod ToDomain() + public TokenizedPaymentMethod ToDomain() => new() { - return new TokenizedPaymentMethod - { - Type = TokenizablePaymentMethodTypeExtensions.From(Type), - Token = Token - }; - } + Type = TokenizablePaymentMethodTypeExtensions.From(Type), + Token = Token + }; } diff --git a/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs index f540957a1a..2a54313421 100644 --- a/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs +++ b/src/Api/Billing/Models/Requests/Payment/TokenizedPaymentMethodRequest.cs @@ -1,31 +1,15 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; -using Bit.Api.Billing.Attributes; -using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Payment.Models; namespace Bit.Api.Billing.Models.Requests.Payment; -public class TokenizedPaymentMethodRequest +public class TokenizedPaymentMethodRequest : MinimalTokenizedPaymentMethodRequest { - [Required] - [PaymentMethodTypeValidation] - public required string Type { get; set; } - - [Required] - public required string Token { get; set; } - public MinimalBillingAddressRequest? BillingAddress { get; set; } - public (TokenizedPaymentMethod, BillingAddress?) ToDomain() + public new (TokenizedPaymentMethod, BillingAddress?) ToDomain() { - var paymentMethod = new TokenizedPaymentMethod - { - Type = TokenizablePaymentMethodTypeExtensions.From(Type), - Token = Token - }; - + var paymentMethod = base.ToDomain(); var billingAddress = BillingAddress?.ToDomain(); - return (paymentMethod, billingAddress); } } diff --git a/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs index b958057f5b..03f20ec9c1 100644 --- a/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs +++ b/src/Api/Billing/Models/Requests/Premium/PremiumCloudHostedSubscriptionRequest.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Api.Billing.Models.Requests.Payment; using Bit.Core.Billing.Payment.Models; diff --git a/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs b/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs deleted file mode 100644 index a3fda0fd6c..0000000000 --- a/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable -using System.ComponentModel.DataAnnotations; -using Bit.Core.Billing.Enums; - -namespace Bit.Api.Billing.Models.Requests; - -public class PreviewTaxAmountForOrganizationTrialRequestBody -{ - [Required] - public PlanType PlanType { get; set; } - - [Required] - public ProductType ProductType { get; set; } - - [Required] public TaxInformationDTO TaxInformation { get; set; } = null!; - - public class TaxInformationDTO - { - [Required] - public string Country { get; set; } = null!; - - [Required] - public string PostalCode { get; set; } = null!; - - public string? TaxId { get; set; } - } -} diff --git a/src/Api/Billing/Models/Requests/Subscriptions/RestartSubscriptionRequest.cs b/src/Api/Billing/Models/Requests/Subscriptions/RestartSubscriptionRequest.cs new file mode 100644 index 0000000000..ac66270427 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Subscriptions/RestartSubscriptionRequest.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Subscriptions; + +public class RestartSubscriptionRequest +{ + [Required] + public required MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; } + [Required] + public required CheckoutBillingAddressRequest BillingAddress { get; set; } + + public (TokenizedPaymentMethod, BillingAddress) ToDomain() + => (PaymentMethod.ToDomain(), BillingAddress.ToDomain()); +} diff --git a/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPlanChangeTaxRequest.cs b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPlanChangeTaxRequest.cs new file mode 100644 index 0000000000..9233a53c85 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPlanChangeTaxRequest.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Models.Requests.Organizations; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Tax; + +public record PreviewOrganizationSubscriptionPlanChangeTaxRequest +{ + [Required] + public required OrganizationSubscriptionPlanChangeRequest Plan { get; set; } + + [Required] + public required CheckoutBillingAddressRequest BillingAddress { get; set; } + + public (OrganizationSubscriptionPlanChange, BillingAddress) ToDomain() => + (Plan.ToDomain(), BillingAddress.ToDomain()); +} diff --git a/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPurchaseTaxRequest.cs b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPurchaseTaxRequest.cs new file mode 100644 index 0000000000..dcc5911f3d --- /dev/null +++ b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionPurchaseTaxRequest.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Models.Requests.Organizations; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Tax; + +public record PreviewOrganizationSubscriptionPurchaseTaxRequest +{ + [Required] + public required OrganizationSubscriptionPurchaseRequest Purchase { get; set; } + + [Required] + public required CheckoutBillingAddressRequest BillingAddress { get; set; } + + public (OrganizationSubscriptionPurchase, BillingAddress) ToDomain() => + (Purchase.ToDomain(), BillingAddress.ToDomain()); +} diff --git a/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionUpdateTaxRequest.cs b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionUpdateTaxRequest.cs new file mode 100644 index 0000000000..ae96214ae3 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Tax/PreviewOrganizationSubscriptionUpdateTaxRequest.cs @@ -0,0 +1,11 @@ +using Bit.Api.Billing.Models.Requests.Organizations; +using Bit.Core.Billing.Organizations.Models; + +namespace Bit.Api.Billing.Models.Requests.Tax; + +public class PreviewOrganizationSubscriptionUpdateTaxRequest +{ + public required OrganizationSubscriptionUpdateRequest Update { get; set; } + + public OrganizationSubscriptionUpdate ToDomain() => Update.ToDomain(); +} diff --git a/src/Api/Billing/Models/Requests/Tax/PreviewPremiumSubscriptionPurchaseTaxRequest.cs b/src/Api/Billing/Models/Requests/Tax/PreviewPremiumSubscriptionPurchaseTaxRequest.cs new file mode 100644 index 0000000000..76b8a5a444 --- /dev/null +++ b/src/Api/Billing/Models/Requests/Tax/PreviewPremiumSubscriptionPurchaseTaxRequest.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Billing.Models.Requests.Payment; +using Bit.Core.Billing.Payment.Models; + +namespace Bit.Api.Billing.Models.Requests.Tax; + +public record PreviewPremiumSubscriptionPurchaseTaxRequest +{ + [Required] + [Range(0, 99, ErrorMessage = "Additional storage must be between 0 and 99 GB.")] + public short AdditionalStorage { get; set; } + + [Required] + public required MinimalBillingAddressRequest BillingAddress { get; set; } + + public (short, BillingAddress) ToDomain() => (AdditionalStorage, BillingAddress.ToDomain()); +} diff --git a/src/Core/AdminConsole/Services/IProviderService.cs b/src/Core/AdminConsole/Services/IProviderService.cs index 66c49d90c6..2b954346ae 100644 --- a/src/Core/AdminConsole/Services/IProviderService.cs +++ b/src/Core/AdminConsole/Services/IProviderService.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -11,8 +11,7 @@ namespace Bit.Core.AdminConsole.Services; public interface IProviderService { - Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource = null); + Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress); Task UpdateAsync(Provider provider, bool updateBilling = false); Task> InviteUserAsync(ProviderUserInvite invite); diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs index 2bf4a54a87..3782b30e3f 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -11,7 +11,7 @@ namespace Bit.Core.AdminConsole.Services.NoopImplementations; public class NoopProviderService : IProviderService { - public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) => throw new NotImplementedException(); + public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress) => throw new NotImplementedException(); public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException(); diff --git a/src/Core/Billing/Commands/BillingCommandResult.cs b/src/Core/Billing/Commands/BillingCommandResult.cs index 3238ab4107..db260e7038 100644 --- a/src/Core/Billing/Commands/BillingCommandResult.cs +++ b/src/Core/Billing/Commands/BillingCommandResult.cs @@ -1,5 +1,4 @@ -#nullable enable -using OneOf; +using OneOf; namespace Bit.Core.Billing.Commands; @@ -20,18 +19,38 @@ public record Unhandled(Exception? Exception = null, string Response = "Somethin /// /// /// The successful result type of the operation. -public class BillingCommandResult : OneOfBase +public class BillingCommandResult(OneOf input) + : OneOfBase(input) { - private BillingCommandResult(OneOf input) : base(input) { } - public static implicit operator BillingCommandResult(T output) => new(output); public static implicit operator BillingCommandResult(BadRequest badRequest) => new(badRequest); public static implicit operator BillingCommandResult(Conflict conflict) => new(conflict); public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); + public BillingCommandResult Map(Func f) + => Match( + value => new BillingCommandResult(f(value)), + badRequest => new BillingCommandResult(badRequest), + conflict => new BillingCommandResult(conflict), + unhandled => new BillingCommandResult(unhandled)); + public Task TapAsync(Func f) => Match( f, _ => Task.CompletedTask, _ => Task.CompletedTask, _ => Task.CompletedTask); } + +public static class BillingCommandResultExtensions +{ + public static async Task> AndThenAsync( + this Task> task, Func>> binder) + { + var result = await task; + return await result.Match( + binder, + badRequest => Task.FromResult(new BillingCommandResult(badRequest)), + conflict => Task.FromResult(new BillingCommandResult(conflict)), + unhandled => Task.FromResult(new BillingCommandResult(unhandled))); + } +} diff --git a/src/Core/Billing/Enums/PlanCadenceType.cs b/src/Core/Billing/Enums/PlanCadenceType.cs new file mode 100644 index 0000000000..9e6fa69832 --- /dev/null +++ b/src/Core/Billing/Enums/PlanCadenceType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Billing.Enums; + +public enum PlanCadenceType +{ + Annually, + Monthly +} diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index b4e37f0151..7aec422a4b 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -9,7 +9,7 @@ using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; -using Bit.Core.Billing.Tax.Commands; +using Bit.Core.Billing.Subscriptions.Commands; using Bit.Core.Billing.Tax.Services; using Bit.Core.Billing.Tax.Services.Implementations; @@ -28,11 +28,12 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddLicenseServices(); services.AddPricingClient(); - services.AddTransient(); services.AddPaymentOperations(); services.AddOrganizationLicenseCommandsQueries(); services.AddPremiumCommands(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); } private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services) @@ -46,5 +47,6 @@ public static class ServiceCollectionExtensions { services.AddScoped(); services.AddScoped(); + services.AddTransient(); } } diff --git a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs new file mode 100644 index 0000000000..041e9bdbad --- /dev/null +++ b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs @@ -0,0 +1,383 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Commands; +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.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using OneOf; +using Stripe; + +namespace Bit.Core.Billing.Organizations.Commands; + +using static Core.Constants; +using static StripeConstants; + +public interface IPreviewOrganizationTaxCommand +{ + Task> Run( + OrganizationSubscriptionPurchase purchase, + BillingAddress billingAddress); + + Task> Run( + Organization organization, + OrganizationSubscriptionPlanChange planChange, + BillingAddress billingAddress); + + Task> Run( + Organization organization, + OrganizationSubscriptionUpdate update); +} + +public class PreviewOrganizationTaxCommand( + ILogger logger, + IPricingClient pricingClient, + IStripeAdapter stripeAdapter) + : BaseBillingCommand(logger), IPreviewOrganizationTaxCommand +{ + public Task> Run( + OrganizationSubscriptionPurchase purchase, + BillingAddress billingAddress) + => HandleAsync<(decimal, decimal)>(async () => + { + var plan = await pricingClient.GetPlanOrThrow(purchase.PlanType); + + var options = GetBaseOptions(billingAddress, purchase.Tier != ProductTierType.Families); + + var items = new List(); + + switch (purchase) + { + case { PasswordManager.Sponsored: true }: + var sponsoredPlan = StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise); + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = sponsoredPlan.StripePlanId, + Quantity = 1 + }); + break; + + case { SecretsManager.Standalone: true }: + items.AddRange([ + new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.PasswordManager.StripeSeatPlanId, + Quantity = purchase.PasswordManager.Seats + }, + new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeSeatPlanId, + Quantity = purchase.SecretsManager.Seats + } + ]); + options.Coupon = CouponIDs.SecretsManagerStandalone; + break; + + default: + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.HasNonSeatBasedPasswordManagerPlan() + ? plan.PasswordManager.StripePlanId + : plan.PasswordManager.StripeSeatPlanId, + Quantity = purchase.PasswordManager.Seats + }); + + if (purchase.PasswordManager.AdditionalStorage > 0) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.PasswordManager.StripeStoragePlanId, + Quantity = purchase.PasswordManager.AdditionalStorage + }); + } + + if (purchase.SecretsManager is { Seats: > 0 }) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeSeatPlanId, + Quantity = purchase.SecretsManager.Seats + }); + + if (purchase.SecretsManager.AdditionalServiceAccounts > 0) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeServiceAccountPlanId, + Quantity = purchase.SecretsManager.AdditionalServiceAccounts + }); + } + } + + break; + } + + options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items }; + + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return GetAmounts(invoice); + }); + + public Task> Run( + Organization organization, + OrganizationSubscriptionPlanChange planChange, + BillingAddress billingAddress) + => HandleAsync<(decimal, decimal)>(async () => + { + if (organization.PlanType.GetProductTier() == ProductTierType.Free) + { + var options = GetBaseOptions(billingAddress, planChange.Tier != ProductTierType.Families); + + var newPlan = await pricingClient.GetPlanOrThrow(planChange.PlanType); + + var items = new List + { + new () + { + Price = newPlan.HasNonSeatBasedPasswordManagerPlan() + ? newPlan.PasswordManager.StripePlanId + : newPlan.PasswordManager.StripeSeatPlanId, + Quantity = 2 + } + }; + + if (organization.UseSecretsManager && planChange.Tier != ProductTierType.Families) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = newPlan.SecretsManager.StripeSeatPlanId, + Quantity = 2 + }); + } + + options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items }; + + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return GetAmounts(invoice); + } + else + { + if (organization is not + { + GatewayCustomerId: not null, + GatewaySubscriptionId: not null + }) + { + return new BadRequest("Organization does not have a subscription."); + } + + var options = GetBaseOptions(billingAddress, planChange.Tier != ProductTierType.Families); + + var subscription = await stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId, + new SubscriptionGetOptions { Expand = ["customer"] }); + + if (subscription.Customer.Discount != null) + { + options.Coupon = subscription.Customer.Discount.Coupon.Id; + } + + var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType); + var newPlan = await pricingClient.GetPlanOrThrow(planChange.PlanType); + + var subscriptionItemsByPriceId = + subscription.Items.ToDictionary(subscriptionItem => subscriptionItem.Price.Id); + + var items = new List(); + + var passwordManagerSeats = subscriptionItemsByPriceId[ + currentPlan.HasNonSeatBasedPasswordManagerPlan() + ? currentPlan.PasswordManager.StripePlanId + : currentPlan.PasswordManager.StripeSeatPlanId]; + + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = newPlan.HasNonSeatBasedPasswordManagerPlan() + ? newPlan.PasswordManager.StripePlanId + : newPlan.PasswordManager.StripeSeatPlanId, + Quantity = passwordManagerSeats.Quantity + }); + + var hasStorage = + subscriptionItemsByPriceId.TryGetValue(newPlan.PasswordManager.StripeStoragePlanId, + out var storage); + + if (hasStorage && storage != null) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = newPlan.PasswordManager.StripeStoragePlanId, + Quantity = storage.Quantity + }); + } + + var hasSecretsManagerSeats = subscriptionItemsByPriceId.TryGetValue( + newPlan.SecretsManager.StripeSeatPlanId, + out var secretsManagerSeats); + + if (hasSecretsManagerSeats && secretsManagerSeats != null) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = newPlan.SecretsManager.StripeSeatPlanId, + Quantity = secretsManagerSeats.Quantity + }); + + var hasServiceAccounts = + subscriptionItemsByPriceId.TryGetValue(newPlan.SecretsManager.StripeServiceAccountPlanId, + out var serviceAccounts); + + if (hasServiceAccounts && serviceAccounts != null) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = newPlan.SecretsManager.StripeServiceAccountPlanId, + Quantity = serviceAccounts.Quantity + }); + } + } + + options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items }; + + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return GetAmounts(invoice); + } + }); + + public Task> Run( + Organization organization, + OrganizationSubscriptionUpdate update) + => HandleAsync<(decimal, decimal)>(async () => + { + if (organization is not + { + GatewayCustomerId: not null, + GatewaySubscriptionId: not null + }) + { + return new BadRequest("Organization does not have a subscription."); + } + + var subscription = await stripeAdapter.SubscriptionGetAsync(organization.GatewaySubscriptionId, + new SubscriptionGetOptions { Expand = ["customer.tax_ids"] }); + + var options = GetBaseOptions(subscription.Customer, + organization.GetProductUsageType() == ProductUsageType.Business); + + if (subscription.Customer.Discount != null) + { + options.Coupon = subscription.Customer.Discount.Coupon.Id; + } + + var currentPlan = await pricingClient.GetPlanOrThrow(organization.PlanType); + + var items = new List(); + + if (update.PasswordManager?.Seats != null) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = currentPlan.HasNonSeatBasedPasswordManagerPlan() + ? currentPlan.PasswordManager.StripePlanId + : currentPlan.PasswordManager.StripeSeatPlanId, + Quantity = update.PasswordManager.Seats + }); + } + + if (update.PasswordManager?.AdditionalStorage is > 0) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = currentPlan.PasswordManager.StripeStoragePlanId, + Quantity = update.PasswordManager.AdditionalStorage + }); + } + + if (update.SecretsManager?.Seats is > 0) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = currentPlan.SecretsManager.StripeSeatPlanId, + Quantity = update.SecretsManager.Seats + }); + + if (update.SecretsManager.AdditionalServiceAccounts is > 0) + { + items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = currentPlan.SecretsManager.StripeServiceAccountPlanId, + Quantity = update.SecretsManager.AdditionalServiceAccounts + }); + } + } + + options.SubscriptionDetails = new InvoiceSubscriptionDetailsOptions { Items = items }; + + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return GetAmounts(invoice); + }); + + private static (decimal, decimal) GetAmounts(Invoice invoice) => ( + Convert.ToDecimal(invoice.Tax) / 100, + Convert.ToDecimal(invoice.Total) / 100); + + private static InvoiceCreatePreviewOptions GetBaseOptions( + OneOf addressChoice, + bool businessUse) + { + var country = addressChoice.Match( + customer => customer.Address.Country, + billingAddress => billingAddress.Country + ); + + var postalCode = addressChoice.Match( + customer => customer.Address.PostalCode, + billingAddress => billingAddress.PostalCode); + + var options = new InvoiceCreatePreviewOptions + { + AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }, + Currency = "usd", + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions { Country = country, PostalCode = postalCode }, + TaxExempt = businessUse && country != CountryAbbreviations.UnitedStates + ? TaxExempt.Reverse + : TaxExempt.None + } + }; + + var taxId = addressChoice.Match( + customer => + { + var taxId = customer.TaxIds?.FirstOrDefault(); + return taxId != null ? new TaxID(taxId.Type, taxId.Value) : null; + }, + billingAddress => billingAddress.TaxId); + + if (taxId == null) + { + return options; + } + + options.CustomerDetails.TaxIds = + [ + new InvoiceCustomerDetailsTaxIdOptions { Type = taxId.Code, Value = taxId.Value } + ]; + + if (taxId.Code == TaxIdType.SpanishNIF) + { + options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions + { + Type = TaxIdType.EUVAT, + Value = $"ES{taxId.Value}" + }); + } + + return options; + } +} diff --git a/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPlanChange.cs b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPlanChange.cs new file mode 100644 index 0000000000..7781f91960 --- /dev/null +++ b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPlanChange.cs @@ -0,0 +1,23 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Organizations.Models; + +public record OrganizationSubscriptionPlanChange +{ + public ProductTierType Tier { get; init; } + public PlanCadenceType Cadence { get; init; } + + public PlanType PlanType => + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + Tier switch + { + ProductTierType.Families => PlanType.FamiliesAnnually, + ProductTierType.Teams => Cadence == PlanCadenceType.Monthly + ? PlanType.TeamsMonthly + : PlanType.TeamsAnnually, + ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly + ? PlanType.EnterpriseMonthly + : PlanType.EnterpriseAnnually, + _ => throw new InvalidOperationException("Cannot change an Organization subscription to a tier that isn't Families, Teams or Enterprise.") + }; +} diff --git a/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPurchase.cs b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPurchase.cs new file mode 100644 index 0000000000..6691d69848 --- /dev/null +++ b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionPurchase.cs @@ -0,0 +1,39 @@ +using Bit.Core.Billing.Enums; + +namespace Bit.Core.Billing.Organizations.Models; + +public record OrganizationSubscriptionPurchase +{ + public ProductTierType Tier { get; init; } + public PlanCadenceType Cadence { get; init; } + public required PasswordManagerSelections PasswordManager { get; init; } + public SecretsManagerSelections? SecretsManager { get; init; } + + public PlanType PlanType => + // ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault + Tier switch + { + ProductTierType.Families => PlanType.FamiliesAnnually, + ProductTierType.Teams => Cadence == PlanCadenceType.Monthly + ? PlanType.TeamsMonthly + : PlanType.TeamsAnnually, + ProductTierType.Enterprise => Cadence == PlanCadenceType.Monthly + ? PlanType.EnterpriseMonthly + : PlanType.EnterpriseAnnually, + _ => throw new InvalidOperationException("Cannot purchase an Organization subscription that isn't Families, Teams or Enterprise.") + }; + + public record PasswordManagerSelections + { + public int Seats { get; init; } + public int AdditionalStorage { get; init; } + public bool Sponsored { get; init; } + } + + public record SecretsManagerSelections + { + public int Seats { get; init; } + public int AdditionalServiceAccounts { get; init; } + public bool Standalone { get; init; } + } +} diff --git a/src/Core/Billing/Organizations/Models/OrganizationSubscriptionUpdate.cs b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionUpdate.cs new file mode 100644 index 0000000000..810f292c81 --- /dev/null +++ b/src/Core/Billing/Organizations/Models/OrganizationSubscriptionUpdate.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Billing.Organizations.Models; + +public record OrganizationSubscriptionUpdate +{ + public PasswordManagerSelections? PasswordManager { get; init; } + public SecretsManagerSelections? SecretsManager { get; init; } + + public record PasswordManagerSelections + { + public int? Seats { get; init; } + public int? AdditionalStorage { get; init; } + } + + public record SecretsManagerSelections + { + public int? Seats { get; init; } + public int? AdditionalServiceAccounts { get; init; } + } +} diff --git a/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs new file mode 100644 index 0000000000..a0b4fcabc2 --- /dev/null +++ b/src/Core/Billing/Premium/Commands/PreviewPremiumTaxCommand.cs @@ -0,0 +1,65 @@ +using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Premium.Commands; + +using static StripeConstants; + +public interface IPreviewPremiumTaxCommand +{ + Task> Run( + int additionalStorage, + BillingAddress billingAddress); +} + +public class PreviewPremiumTaxCommand( + ILogger logger, + IStripeAdapter stripeAdapter) : BaseBillingCommand(logger), IPreviewPremiumTaxCommand +{ + public Task> Run( + int additionalStorage, + BillingAddress billingAddress) + => HandleAsync<(decimal, decimal)>(async () => + { + var options = new InvoiceCreatePreviewOptions + { + AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }, + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions + { + Country = billingAddress.Country, + PostalCode = billingAddress.PostalCode + } + }, + Currency = "usd", + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions + { + Items = + [ + new InvoiceSubscriptionDetailsItemOptions { Price = Prices.PremiumAnnually, Quantity = 1 } + ] + } + }; + + if (additionalStorage > 0) + { + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = Prices.StoragePlanPersonal, + Quantity = additionalStorage + }); + } + + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return GetAmounts(invoice); + }); + + private static (decimal, decimal) GetAmounts(Invoice invoice) => ( + Convert.ToDecimal(invoice.Tax) / 100, + Convert.ToDecimal(invoice.Total) / 100); +} diff --git a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs index 07a057d40c..e155b427f1 100644 --- a/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs @@ -258,7 +258,7 @@ public class ProviderMigrator( // 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, taxInfo, dummyPaymentSource); + var customer = await providerBillingService.SetupCustomer(provider, null, null); await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions { diff --git a/src/Core/Billing/Providers/Services/IProviderBillingService.cs b/src/Core/Billing/Providers/Services/IProviderBillingService.cs index 173249f79f..57d68db038 100644 --- a/src/Core/Billing/Providers/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Providers/Services/IProviderBillingService.cs @@ -5,10 +5,10 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Tax.Models; -using Bit.Core.Models.Business; using Stripe; namespace Bit.Core.Billing.Providers.Services; @@ -79,16 +79,16 @@ public interface IProviderBillingService int seatAdjustment); /// - /// For use during the provider setup process, this method creates a Stripe for the specified utilizing the provided . + /// For use during the provider setup process, this method creates a Stripe for the specified utilizing the provided and . /// /// The to create a Stripe customer for. - /// The to use for calculating the customer's automatic tax. - /// The (ex. Credit Card) to attach to the customer. + /// The (e.g., Credit Card, Bank Account, or PayPal) to attach to the customer. + /// The containing the customer's billing information including address and tax ID details. /// The newly created for the . Task SetupCustomer( Provider provider, - TaxInfo taxInfo, - TokenizedPaymentSource tokenizedPaymentSource); + TokenizedPaymentMethod paymentMethod, + BillingAddress billingAddress); /// /// For use during the provider setup process, this method starts a Stripe for the given . diff --git a/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs new file mode 100644 index 0000000000..351c75ace0 --- /dev/null +++ b/src/Core/Billing/Subscriptions/Commands/RestartSubscriptionCommand.cs @@ -0,0 +1,92 @@ +using Bit.Core.AdminConsole.Entities; +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.Services; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Services; +using OneOf.Types; +using Stripe; + +namespace Bit.Core.Billing.Subscriptions.Commands; + +using static StripeConstants; + +public interface IRestartSubscriptionCommand +{ + Task> Run( + ISubscriber subscriber); +} + +public class RestartSubscriptionCommand( + IOrganizationRepository organizationRepository, + IProviderRepository providerRepository, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService, + IUserRepository userRepository) : IRestartSubscriptionCommand +{ + public async Task> Run( + ISubscriber subscriber) + { + var existingSubscription = await subscriberService.GetSubscription(subscriber); + + if (existingSubscription is not { Status: SubscriptionStatus.Canceled }) + { + return new BadRequest("Cannot restart a subscription that is not canceled."); + } + + var options = new SubscriptionCreateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, + CollectionMethod = CollectionMethod.ChargeAutomatically, + Customer = existingSubscription.CustomerId, + Items = existingSubscription.Items.Select(subscriptionItem => new SubscriptionItemOptions + { + Price = subscriptionItem.Price.Id, + Quantity = subscriptionItem.Quantity + }).ToList(), + Metadata = existingSubscription.Metadata, + OffSession = true, + TrialPeriodDays = 0 + }; + + var subscription = await stripeAdapter.SubscriptionCreateAsync(options); + await EnableAsync(subscriber, subscription); + return new None(); + } + + private async Task EnableAsync(ISubscriber subscriber, Subscription subscription) + { + switch (subscriber) + { + case Organization organization: + { + organization.GatewaySubscriptionId = subscription.Id; + organization.Enabled = true; + organization.ExpirationDate = subscription.CurrentPeriodEnd; + organization.RevisionDate = DateTime.UtcNow; + await organizationRepository.ReplaceAsync(organization); + break; + } + case Provider provider: + { + provider.GatewaySubscriptionId = subscription.Id; + provider.Enabled = true; + provider.RevisionDate = DateTime.UtcNow; + await providerRepository.ReplaceAsync(provider); + break; + } + case User user: + { + user.GatewaySubscriptionId = subscription.Id; + user.Premium = true; + user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + user.RevisionDate = DateTime.UtcNow; + await userRepository.ReplaceAsync(user); + break; + } + } + } +} diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs deleted file mode 100644 index 94d3724d73..0000000000 --- a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs +++ /dev/null @@ -1,136 +0,0 @@ -using Bit.Core.Billing.Commands; -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Services; -using Bit.Core.Services; -using Microsoft.Extensions.Logging; -using Stripe; - -namespace Bit.Core.Billing.Tax.Commands; - -public interface IPreviewTaxAmountCommand -{ - Task> Run(OrganizationTrialParameters parameters); -} - -public class PreviewTaxAmountCommand( - ILogger logger, - IPricingClient pricingClient, - IStripeAdapter stripeAdapter, - ITaxService taxService) : BaseBillingCommand(logger), IPreviewTaxAmountCommand -{ - protected override Conflict DefaultConflict - => new("We had a problem calculating your tax obligation. Please contact support for assistance."); - - public Task> Run(OrganizationTrialParameters parameters) - => HandleAsync(async () => - { - var (planType, productType, taxInformation) = parameters; - - var plan = await pricingClient.GetPlanOrThrow(planType); - - var options = new InvoiceCreatePreviewOptions - { - Currency = "usd", - CustomerDetails = new InvoiceCustomerDetailsOptions - { - Address = new AddressOptions - { - Country = taxInformation.Country, - PostalCode = taxInformation.PostalCode - } - }, - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = - [ - new InvoiceSubscriptionDetailsItemOptions - { - Price = plan.HasNonSeatBasedPasswordManagerPlan() - ? plan.PasswordManager.StripePlanId - : plan.PasswordManager.StripeSeatPlanId, - Quantity = 1 - } - ] - } - }; - - if (productType == ProductType.SecretsManager) - { - options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions - { - Price = plan.SecretsManager.StripeSeatPlanId, - Quantity = 1 - }); - - options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone; - } - - if (!string.IsNullOrEmpty(taxInformation.TaxId)) - { - var taxIdType = taxService.GetStripeTaxCode( - taxInformation.Country, - taxInformation.TaxId); - - if (string.IsNullOrEmpty(taxIdType)) - { - return new BadRequest( - "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance."); - } - - options.CustomerDetails.TaxIds = - [ - new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = taxInformation.TaxId } - ]; - - if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) - { - options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions - { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{parameters.TaxInformation.TaxId}" - }); - } - } - - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - if (parameters.PlanType.IsBusinessProductTierType() && - parameters.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates) - { - options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse; - } - - var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); - return Convert.ToDecimal(invoice.Tax) / 100; - }); -} - -#region Command Parameters - -public record OrganizationTrialParameters -{ - public required PlanType PlanType { get; set; } - public required ProductType ProductType { get; set; } - public required TaxInformationDTO TaxInformation { get; set; } - - public void Deconstruct( - out PlanType planType, - out ProductType productType, - out TaxInformationDTO taxInformation) - { - planType = PlanType; - productType = ProductType; - taxInformation = TaxInformation; - } - - public record TaxInformationDTO - { - public required string Country { get; set; } - public required string PostalCode { get; set; } - public string? TaxId { get; set; } - } -} - -#endregion diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index d26e0f67fa..bcc9b0b40d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -169,7 +169,6 @@ public static class FeatureFlagKeys public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; - public const string PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout"; public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover"; public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings"; public const string PM23385_UseNewPremiumFlow = "pm-23385-use-new-premium-flow"; diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 03d1776e90..4863baf73e 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -3,6 +3,7 @@ using Bit.Core.Models.BitStripe; using Stripe; +using Stripe.Tax; namespace Bit.Core.Services; @@ -23,6 +24,7 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.TestHelpers.TestClockService _testClockService; private readonly CustomerBalanceTransactionService _customerBalanceTransactionService; private readonly Stripe.Tax.RegistrationService _taxRegistrationService; + private readonly CalculationService _calculationService; public StripeAdapter() { @@ -41,6 +43,7 @@ public class StripeAdapter : IStripeAdapter _testClockService = new Stripe.TestHelpers.TestClockService(); _customerBalanceTransactionService = new CustomerBalanceTransactionService(); _taxRegistrationService = new Stripe.Tax.RegistrationService(); + _calculationService = new CalculationService(); } public Task CustomerCreateAsync(Stripe.CustomerCreateOptions options) diff --git a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs new file mode 100644 index 0000000000..8e3cd5a0fa --- /dev/null +++ b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs @@ -0,0 +1,1262 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models.StaticStore.Plans; +using Bit.Core.Billing.Organizations.Commands; +using Bit.Core.Billing.Organizations.Models; +using Bit.Core.Billing.Payment.Models; +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; + +namespace Bit.Core.Test.Billing.Organizations.Commands; + +public class PreviewOrganizationTaxCommandTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IPricingClient _pricingClient = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly PreviewOrganizationTaxCommand _command; + + public PreviewOrganizationTaxCommandTests() + { + _command = new PreviewOrganizationTaxCommand(_logger, _pricingClient, _stripeAdapter); + } + + #region Subscription Purchase + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_SponsoredPasswordManager_ReturnsCorrectTaxAmounts() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Families, + Cadence = PlanCadenceType.Annually, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 6, + AdditionalStorage = 0, + Sponsored = true + } + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "12345" + }; + + var plan = new FamiliesPlan(); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 500, + Total = 5500 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(5.00m, tax); + Assert.Equal(55.00m, total); + + // Verify the correct Stripe API call for sponsored subscription + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2021-family-for-enterprise-annually" && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_StandaloneSecretsManager_ReturnsCorrectTaxAmounts() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Teams, + Cadence = PlanCadenceType.Monthly, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 5, + AdditionalStorage = 0, + Sponsored = false + }, + SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections + { + Seats = 3, + AdditionalServiceAccounts = 0, + Standalone = true + } + }; + + var billingAddress = new BillingAddress + { + Country = "CA", + PostalCode = "K1A 0A6" + }; + + var plan = new TeamsPlan(false); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 750, + Total = 8250 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(7.50m, tax); + Assert.Equal(82.50m, total); + + // Verify the correct Stripe API call for standalone secrets manager + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "K1A 0A6" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_StandardPurchaseWithStorage_ReturnsCorrectTaxAmounts() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 10, + AdditionalStorage = 5, + Sponsored = false + }, + SecretsManager = new OrganizationSubscriptionPurchase.SecretsManagerSelections + { + Seats = 8, + AdditionalServiceAccounts = 3, + Standalone = false + } + }; + + var billingAddress = new BillingAddress + { + Country = "GB", + PostalCode = "SW1A 1AA", + TaxId = new TaxID("gb_vat", "123456789") + }; + + var plan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 1200, + Total = 12200 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(12.00m, tax); + Assert.Equal(122.00m, total); + + // Verify the correct Stripe API call for comprehensive purchase with storage and service accounts + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "GB" && + options.CustomerDetails.Address.PostalCode == "SW1A 1AA" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.CustomerDetails.TaxIds.Count == 1 && + options.CustomerDetails.TaxIds[0].Type == "gb_vat" && + options.CustomerDetails.TaxIds[0].Value == "123456789" && + options.SubscriptionDetails.Items.Count == 4 && + options.SubscriptionDetails.Items.Any(item => + item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 10) && + options.SubscriptionDetails.Items.Any(item => + item.Price == "storage-gb-annually" && item.Quantity == 5) && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_FamiliesTier_NoSecretsManager_ReturnsCorrectTaxAmounts() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Families, + Cadence = PlanCadenceType.Annually, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 6, + AdditionalStorage = 0, + Sponsored = false + } + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "90210" + }; + + var plan = new FamiliesPlan(); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 300, + Total = 3300 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(3.00m, tax); + Assert.Equal(33.00m, total); + + // Verify the correct Stripe API call for Families tier (non-seat-based plan) + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "90210" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2020-families-org-annually" && + options.SubscriptionDetails.Items[0].Quantity == 6 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_BusinessUseNonUSCountry_UsesTaxExemptReverse() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Teams, + Cadence = PlanCadenceType.Monthly, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 3, + AdditionalStorage = 0, + Sponsored = false + } + }; + + var billingAddress = new BillingAddress + { + Country = "DE", + PostalCode = "10115" + }; + + var plan = new TeamsPlan(false); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 0, + Total = 2700 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(0.00m, tax); + Assert.Equal(27.00m, total); + + // Verify the correct Stripe API call for business use in non-US country (tax exempt reverse) + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "DE" && + options.CustomerDetails.Address.PostalCode == "10115" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && + options.SubscriptionDetails.Items[0].Quantity == 3 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionPurchase_SpanishNIFTaxId_AddsEUVATTaxId() + { + var purchase = new OrganizationSubscriptionPurchase + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Monthly, + PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections + { + Seats = 15, + AdditionalStorage = 0, + Sponsored = false + } + }; + + var billingAddress = new BillingAddress + { + Country = "ES", + PostalCode = "28001", + TaxId = new TaxID(TaxIdType.SpanishNIF, "12345678Z") + }; + + var plan = new EnterprisePlan(false); + _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 2100, + Total = 12100 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(purchase, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(21.00m, tax); + Assert.Equal(121.00m, total); + + // Verify the correct Stripe API call for Spanish NIF that adds both Spanish NIF and EU VAT tax IDs + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "ES" && + options.CustomerDetails.Address.PostalCode == "28001" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.CustomerDetails.TaxIds.Count == 2 && + options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.SpanishNIF && t.Value == "12345678Z") && + options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == "ES12345678Z") && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-enterprise-seat-monthly" && + options.SubscriptionDetails.Items[0].Quantity == 15 && + options.Coupon == null)); + } + + #endregion + + #region Subscription Plan Change + + [Fact] + public async Task Run_OrganizationPlanChange_FreeOrganizationToTeams_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.Free, + UseSecretsManager = false + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Teams, + Cadence = PlanCadenceType.Monthly + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "12345" + }; + + var plan = new TeamsPlan(false); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 120, + Total = 1320 + }; + + _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(1.20m, tax); + Assert.Equal(13.20m, total); + + // Verify the correct Stripe API call for free organization upgrade to Teams + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && + options.SubscriptionDetails.Items[0].Quantity == 2 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_FreeOrganizationToFamilies_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.Free, + UseSecretsManager = true + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Families, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "CA", + PostalCode = "K1A 0A6" + }; + + var plan = new FamiliesPlan(); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 400, + Total = 4400 + }; + + _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(4.00m, tax); + Assert.Equal(44.00m, total); + + // Verify the correct Stripe API call for free organization upgrade to Families (no SM for Families) + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "K1A 0A6" && + 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.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_FreeOrganizationWithSecretsManagerToEnterprise_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.Free, + UseSecretsManager = true + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "GB", + PostalCode = "SW1A 1AA" + }; + + var plan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(plan); + + var invoice = new Invoice + { + Tax = 800, + Total = 8800 + }; + + _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(8.00m, tax); + Assert.Equal(88.00m, total); + + // Verify the correct Stripe API call for free organization with SM to Enterprise + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "GB" && + options.CustomerDetails.Address.PostalCode == "SW1A 1AA" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_ExistingSubscriptionUpgrade_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123", + UseSecretsManager = true + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "DE", + PostalCode = "10115" + }; + + var currentPlan = new TeamsPlan(false); + var newPlan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan); + + // Mock existing subscription with items - using NEW plan IDs since command looks for new plan prices + var subscriptionItems = new List + { + new() { Price = new Price { Id = "2023-teams-org-seat-monthly" }, Quantity = 8 }, + new() { Price = new Price { Id = "storage-gb-annually" }, Quantity = 3 }, + new() { Price = new Price { Id = "secrets-manager-enterprise-seat-annually" }, Quantity = 5 }, + new() { Price = new Price { Id = "secrets-manager-service-account-2024-annually" }, Quantity = 10 } + }; + + 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 = 1500, + Total = 16500 + }; + + _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(15.00m, tax); + Assert.Equal(165.00m, total); + + // Verify the correct Stripe API call for existing subscription upgrade + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "DE" && + options.CustomerDetails.Address.PostalCode == "10115" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 4 && + options.SubscriptionDetails.Items.Any(item => + item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 8) && + options.SubscriptionDetails.Items.Any(item => + item.Price == "storage-gb-annually" && item.Quantity == 3) && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationPlanChange_ExistingSubscriptionWithDiscount_PreservesCoupon() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123", + UseSecretsManager = false + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "90210" + }; + + var currentPlan = new TeamsPlan(true); + var newPlan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(currentPlan); + _pricingClient.GetPlanOrThrow(planChange.PlanType).Returns(newPlan); + + // Mock existing subscription with discount + var subscriptionItems = new List + { + new() { Price = new Price { Id = "2023-teams-org-seat-annually" }, Quantity = 5 } + }; + + var subscription = new Subscription + { + Id = "sub_test123", + Items = new StripeList { Data = subscriptionItems }, + Customer = new Customer + { + Discount = new Discount + { + Coupon = new Coupon { Id = "EXISTING_DISCOUNT_50" } + } + } + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 600, + Total = 6600 + }; + + _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(6.00m, tax); + Assert.Equal(66.00m, total); + + // Verify the correct Stripe API call preserves existing discount + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "90210" && + 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 == 5 && + options.Coupon == "EXISTING_DISCOUNT_50")); + } + + [Fact] + public async Task Run_OrganizationPlanChange_OrganizationWithoutGatewayIds_ReturnsBadRequest() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + GatewayCustomerId = null, + GatewaySubscriptionId = null + }; + + var planChange = new OrganizationSubscriptionPlanChange + { + Tier = ProductTierType.Enterprise, + Cadence = PlanCadenceType.Annually + }; + + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "12345" + }; + + var result = await _command.Run(organization, planChange, billingAddress); + + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Organization does not have a subscription.", badRequest.Response); + + // Verify no Stripe API calls were made + await _stripeAdapter.DidNotReceive().InvoiceCreatePreviewAsync(Arg.Any()); + await _stripeAdapter.DidNotReceive().SubscriptionGetAsync(Arg.Any(), Arg.Any()); + } + + #endregion + + #region Subscription Update + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_PasswordManagerSeatsOnly_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 10, + AdditionalStorage = null + } + }; + + var plan = new TeamsPlan(false); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "US", PostalCode = "12345" }, + Discount = null, + TaxIds = null + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 600, + Total = 6600 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(6.00m, tax); + Assert.Equal(66.00m, total); + + // Verify the correct Stripe API call for PM seats only + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && + options.SubscriptionDetails.Items[0].Quantity == 10 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_PasswordManagerWithStorage_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 15, + AdditionalStorage = 5 + } + }; + + var plan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "CA", PostalCode = "K1A 0A6" }, + Discount = null, + TaxIds = null + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _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, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(12.00m, tax); + Assert.Equal(132.00m, total); + + // Verify the correct Stripe API call for PM seats + storage + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "K1A 0A6" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_SecretsManagerOnly_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections + { + Seats = 8, + AdditionalServiceAccounts = null + } + }; + + var plan = new TeamsPlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "DE", PostalCode = "10115" }, + Discount = null, + TaxIds = null + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 800, + Total = 8800 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(8.00m, tax); + Assert.Equal(88.00m, total); + + // Verify the correct Stripe API call for SM seats only + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "DE" && + options.CustomerDetails.Address.PostalCode == "10115" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "secrets-manager-teams-seat-annually" && + options.SubscriptionDetails.Items[0].Quantity == 8 && + options.Coupon == null)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_SecretsManagerWithServiceAccounts_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseMonthly, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections + { + Seats = 12, + AdditionalServiceAccounts = 20 + } + }; + + var plan = new EnterprisePlan(false); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "GB", PostalCode = "SW1A 1AA" }, + Discount = null, + TaxIds = new StripeList + { + Data = new List + { + new() { Type = "gb_vat", Value = "GB123456789" } + } + } + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 1500, + Total = 16500 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(15.00m, tax); + Assert.Equal(165.00m, total); + + // Verify the correct Stripe API call for SM seats + service accounts with tax ID + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "GB" && + options.CustomerDetails.Address.PostalCode == "SW1A 1AA" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.CustomerDetails.TaxIds.Count == 1 && + options.CustomerDetails.TaxIds[0].Type == "gb_vat" && + options.CustomerDetails.TaxIds[0].Value == "GB123456789" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_ComprehensiveUpdate_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.EnterpriseAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 25, + AdditionalStorage = 10 + }, + SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections + { + Seats = 15, + AdditionalServiceAccounts = 30 + } + }; + + var plan = new EnterprisePlan(true); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "ES", PostalCode = "28001" }, + Discount = new Discount + { + Coupon = new Coupon { Id = "ENTERPRISE_DISCOUNT_20" } + }, + TaxIds = new StripeList + { + Data = new List + { + new() { Type = TaxIdType.SpanishNIF, Value = "12345678Z" } + } + } + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 2500, + Total = 27500 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(25.00m, tax); + Assert.Equal(275.00m, total); + + // Verify the correct Stripe API call for comprehensive update with discount and Spanish tax ID + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "ES" && + options.CustomerDetails.Address.PostalCode == "28001" && + options.CustomerDetails.TaxExempt == TaxExempt.Reverse && + options.CustomerDetails.TaxIds.Count == 2 && + options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.SpanishNIF && t.Value == "12345678Z") && + options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == "ES12345678Z") && + options.SubscriptionDetails.Items.Count == 4 && + options.SubscriptionDetails.Items.Any(item => + item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 25) && + options.SubscriptionDetails.Items.Any(item => + item.Price == "storage-gb-annually" && item.Quantity == 10) && + options.SubscriptionDetails.Items.Any(item => + 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")); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_FamiliesTierPersonalUsage_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.FamiliesAnnually, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 6, + AdditionalStorage = 2 + } + }; + + var plan = new FamiliesPlan(); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "AU", PostalCode = "2000" }, + Discount = null, + TaxIds = null + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 500, + Total = 5500 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(5.00m, tax); + Assert.Equal(55.00m, total); + + // Verify the correct Stripe API call for Families tier (personal usage, no business tax exemption) + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "AU" && + options.CustomerDetails.Address.PostalCode == "2000" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + 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)); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_OrganizationWithoutGatewayIds_ReturnsBadRequest() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + GatewayCustomerId = null, + GatewaySubscriptionId = null + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 5 + } + }; + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Organization does not have a subscription.", badRequest.Response); + + // Verify no Stripe API calls were made + await _stripeAdapter.DidNotReceive().InvoiceCreatePreviewAsync(Arg.Any()); + await _stripeAdapter.DidNotReceive().SubscriptionGetAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task Run_OrganizationSubscriptionUpdate_ZeroValuesExcluded_ReturnsCorrectTaxAmounts() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + PlanType = PlanType.TeamsMonthly, + GatewayCustomerId = "cus_test123", + GatewaySubscriptionId = "sub_test123" + }; + + var update = new OrganizationSubscriptionUpdate + { + PasswordManager = new OrganizationSubscriptionUpdate.PasswordManagerSelections + { + Seats = 5, + AdditionalStorage = 0 // Should be excluded + }, + SecretsManager = new OrganizationSubscriptionUpdate.SecretsManagerSelections + { + Seats = 0, // Should be excluded entirely (including service accounts) + AdditionalServiceAccounts = 10 + } + }; + + var plan = new TeamsPlan(false); + _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(plan); + + var customer = new Customer + { + Address = new Address { Country = "US", PostalCode = "90210" }, + Discount = null, + TaxIds = null + }; + + var subscription = new Subscription + { + Customer = customer + }; + + _stripeAdapter.SubscriptionGetAsync("sub_test123", Arg.Any()).Returns(subscription); + + var invoice = new Invoice + { + Tax = 300, + Total = 3300 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(organization, update); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(3.00m, tax); + Assert.Equal(33.00m, total); + + // Verify only PM seats are included (storage=0 excluded, SM seats=0 so entire SM excluded) + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "90210" && + options.CustomerDetails.TaxExempt == TaxExempt.None && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && + options.SubscriptionDetails.Items[0].Quantity == 5 && + options.Coupon == null)); + } + + #endregion +} diff --git a/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs new file mode 100644 index 0000000000..bf7d093dc7 --- /dev/null +++ b/test/Core.Test/Billing/Premium/Commands/PreviewPremiumTaxCommandTests.cs @@ -0,0 +1,292 @@ +using Bit.Core.Billing.Payment.Models; +using Bit.Core.Billing.Premium.Commands; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Stripe; +using Xunit; +using static Bit.Core.Billing.Constants.StripeConstants; + +namespace Bit.Core.Test.Billing.Premium.Commands; + +public class PreviewPremiumTaxCommandTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly PreviewPremiumTaxCommand _command; + + public PreviewPremiumTaxCommandTests() + { + _command = new PreviewPremiumTaxCommand(_logger, _stripeAdapter); + } + + [Fact] + public async Task Run_PremiumWithoutStorage_ReturnsCorrectTaxAmounts() + { + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "12345" + }; + + var invoice = new Invoice + { + Tax = 300, + Total = 3300 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(0, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(3.00m, tax); + Assert.Equal(33.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 == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually && + options.SubscriptionDetails.Items[0].Quantity == 1)); + } + + [Fact] + public async Task Run_PremiumWithAdditionalStorage_ReturnsCorrectTaxAmounts() + { + var billingAddress = new BillingAddress + { + Country = "CA", + PostalCode = "K1A 0A6" + }; + + var invoice = new Invoice + { + Tax = 500, + Total = 5500 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(5, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(5.00m, tax); + Assert.Equal(55.00m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "K1A 0A6" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.PremiumAnnually && item.Quantity == 1) && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.StoragePlanPersonal && item.Quantity == 5))); + } + + [Fact] + public async Task Run_PremiumWithZeroStorage_ExcludesStorageFromItems() + { + var billingAddress = new BillingAddress + { + Country = "GB", + PostalCode = "SW1A 1AA" + }; + + var invoice = new Invoice + { + Tax = 250, + Total = 2750 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(0, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(2.50m, tax); + Assert.Equal(27.50m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "GB" && + options.CustomerDetails.Address.PostalCode == "SW1A 1AA" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually && + options.SubscriptionDetails.Items[0].Quantity == 1)); + } + + [Fact] + public async Task Run_PremiumWithLargeStorage_HandlesMultipleStorageUnits() + { + var billingAddress = new BillingAddress + { + Country = "DE", + PostalCode = "10115" + }; + + var invoice = new Invoice + { + Tax = 800, + Total = 8800 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(20, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(8.00m, tax); + Assert.Equal(88.00m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "DE" && + options.CustomerDetails.Address.PostalCode == "10115" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.PremiumAnnually && item.Quantity == 1) && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.StoragePlanPersonal && item.Quantity == 20))); + } + + [Fact] + public async Task Run_PremiumInternationalAddress_UsesCorrectAddressInfo() + { + var billingAddress = new BillingAddress + { + Country = "AU", + PostalCode = "2000" + }; + + var invoice = new Invoice + { + Tax = 450, + Total = 4950 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(10, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(4.50m, tax); + Assert.Equal(49.50m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "AU" && + options.CustomerDetails.Address.PostalCode == "2000" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.PremiumAnnually && item.Quantity == 1) && + options.SubscriptionDetails.Items.Any(item => + item.Price == Prices.StoragePlanPersonal && item.Quantity == 10))); + } + + [Fact] + public async Task Run_PremiumNoTax_ReturnsZeroTax() + { + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "97330" // Example of a tax-free jurisdiction + }; + + var invoice = new Invoice + { + Tax = 0, + Total = 3000 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(0, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(0.00m, tax); + Assert.Equal(30.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 == "97330" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually && + options.SubscriptionDetails.Items[0].Quantity == 1)); + } + + [Fact] + public async Task Run_NegativeStorage_TreatedAsZero() + { + var billingAddress = new BillingAddress + { + Country = "FR", + PostalCode = "75001" + }; + + var invoice = new Invoice + { + Tax = 600, + Total = 6600 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(-5, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(6.00m, tax); + Assert.Equal(66.00m, total); + + await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => + options.AutomaticTax.Enabled == true && + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "FR" && + options.CustomerDetails.Address.PostalCode == "75001" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == Prices.PremiumAnnually && + options.SubscriptionDetails.Items[0].Quantity == 1)); + } + + [Fact] + public async Task Run_AmountConversion_CorrectlyConvertsStripeAmounts() + { + var billingAddress = new BillingAddress + { + Country = "US", + PostalCode = "12345" + }; + + // Stripe amounts are in cents + var invoice = new Invoice + { + Tax = 123, // $1.23 + Total = 3123 // $31.23 + }; + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()).Returns(invoice); + + var result = await _command.Run(0, billingAddress); + + Assert.True(result.IsT0); + var (tax, total) = result.AsT0; + Assert.Equal(1.23m, tax); + Assert.Equal(31.23m, total); + } +} diff --git a/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs new file mode 100644 index 0000000000..a5970c79ab --- /dev/null +++ b/test/Core.Test/Billing/Subscriptions/RestartSubscriptionCommandTests.cs @@ -0,0 +1,198 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Billing.Subscriptions.Commands; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Services; +using NSubstitute; +using Stripe; +using Xunit; + +namespace Bit.Core.Test.Billing.Subscriptions; + +using static StripeConstants; + +public class RestartSubscriptionCommandTests +{ + private readonly IOrganizationRepository _organizationRepository = Substitute.For(); + private readonly IProviderRepository _providerRepository = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ISubscriberService _subscriberService = Substitute.For(); + private readonly IUserRepository _userRepository = Substitute.For(); + private readonly RestartSubscriptionCommand _command; + + public RestartSubscriptionCommandTests() + { + _command = new RestartSubscriptionCommand( + _organizationRepository, + _providerRepository, + _stripeAdapter, + _subscriberService, + _userRepository); + } + + [Fact] + public async Task Run_SubscriptionNotCanceled_ReturnsBadRequest() + { + var organization = new Organization { Id = Guid.NewGuid() }; + + var subscription = new Subscription { Status = SubscriptionStatus.Active }; + _subscriberService.GetSubscription(organization).Returns(subscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Cannot restart a subscription that is not canceled.", badRequest.Response); + } + + [Fact] + public async Task Run_NoExistingSubscription_ReturnsBadRequest() + { + var organization = new Organization { Id = Guid.NewGuid() }; + + _subscriberService.GetSubscription(organization).Returns((Subscription)null); + + var result = await _command.Run(organization); + + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal("Cannot restart a subscription that is not canceled.", badRequest.Response); + } + + [Fact] + public async Task Run_Organization_Success_ReturnsNone() + { + var organizationId = Guid.NewGuid(); + var organization = new Organization { Id = organizationId }; + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123", + Items = new StripeList + { + Data = + [ + new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }, + new SubscriptionItem { Price = new Price { Id = "price_2" }, Quantity = 2 } + ] + }, + Metadata = new Dictionary { ["key"] = "value" } + }; + + var newSubscription = new Subscription + { + Id = "sub_new", + CurrentPeriodEnd = currentPeriodEnd + }; + + _subscriberService.GetSubscription(organization).Returns(existingSubscription); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(organization); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is((SubscriptionCreateOptions options) => + options.AutomaticTax.Enabled == true && + options.CollectionMethod == CollectionMethod.ChargeAutomatically && + options.Customer == "cus_123" && + options.Items.Count == 2 && + options.Items[0].Price == "price_1" && + options.Items[0].Quantity == 1 && + options.Items[1].Price == "price_2" && + options.Items[1].Quantity == 2 && + options.Metadata["key"] == "value" && + options.OffSession == true && + options.TrialPeriodDays == 0)); + + await _organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => + org.Id == organizationId && + org.GatewaySubscriptionId == "sub_new" && + org.Enabled == true && + org.ExpirationDate == currentPeriodEnd)); + } + + [Fact] + public async Task Run_Provider_Success_ReturnsNone() + { + var providerId = Guid.NewGuid(); + var provider = new Provider { Id = providerId }; + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123", + Items = new StripeList + { + Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }] + }, + Metadata = new Dictionary() + }; + + var newSubscription = new Subscription + { + Id = "sub_new", + CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1) + }; + + _subscriberService.GetSubscription(provider).Returns(existingSubscription); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(provider); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); + + await _providerRepository.Received(1).ReplaceAsync(Arg.Is(prov => + prov.Id == providerId && + prov.GatewaySubscriptionId == "sub_new" && + prov.Enabled == true)); + } + + [Fact] + public async Task Run_User_Success_ReturnsNone() + { + var userId = Guid.NewGuid(); + var user = new User { Id = userId }; + var currentPeriodEnd = DateTime.UtcNow.AddMonths(1); + + var existingSubscription = new Subscription + { + Status = SubscriptionStatus.Canceled, + CustomerId = "cus_123", + Items = new StripeList + { + Data = [new SubscriptionItem { Price = new Price { Id = "price_1" }, Quantity = 1 }] + }, + Metadata = new Dictionary() + }; + + var newSubscription = new Subscription + { + Id = "sub_new", + CurrentPeriodEnd = currentPeriodEnd + }; + + _subscriberService.GetSubscription(user).Returns(existingSubscription); + _stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(newSubscription); + + var result = await _command.Run(user); + + Assert.True(result.IsT0); + + await _stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Any()); + + await _userRepository.Received(1).ReplaceAsync(Arg.Is(u => + u.Id == userId && + u.GatewaySubscriptionId == "sub_new" && + u.Premium == true && + u.PremiumExpirationDate == currentPeriodEnd)); + } +} diff --git a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs deleted file mode 100644 index 1de180cea1..0000000000 --- a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs +++ /dev/null @@ -1,541 +0,0 @@ -using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Commands; -using Bit.Core.Billing.Tax.Services; -using Bit.Core.Services; -using Bit.Core.Utilities; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Stripe; -using Xunit; -using static Bit.Core.Billing.Tax.Commands.OrganizationTrialParameters; - -namespace Bit.Core.Test.Billing.Tax.Commands; - -public class PreviewTaxAmountCommandTests -{ - private readonly ILogger _logger = Substitute.For>(); - private readonly IPricingClient _pricingClient = Substitute.For(); - private readonly IStripeAdapter _stripeAdapter = Substitute.For(); - private readonly ITaxService _taxService = Substitute.For(); - - private readonly PreviewTaxAmountCommand _command; - - public PreviewTaxAmountCommandTests() - { - _command = new PreviewTaxAmountCommand(_logger, _pricingClient, _stripeAdapter, _taxService); - } - - [Fact] - public async Task Run_WithSeatBasedPasswordManagerPlan_GetsTaxAmount() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "US" && - options.CustomerDetails.Address.PostalCode == "12345" && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && - options.SubscriptionDetails.Items[0].Quantity == 1 && - options.AutomaticTax.Enabled == true - )) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT0); - var taxAmount = result.AsT0; - Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); - } - - [Fact] - public async Task Run_WithNonSeatBasedPasswordManagerPlan_GetsTaxAmount() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.FamiliesAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "US" && - options.CustomerDetails.Address.PostalCode == "12345" && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripePlanId && - options.SubscriptionDetails.Items[0].Quantity == 1 && - options.AutomaticTax.Enabled == true - )) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT0); - var taxAmount = result.AsT0; - Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); - } - - [Fact] - public async Task Run_WithSecretsManagerPlan_GetsTaxAmount() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.SecretsManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "US" && - options.CustomerDetails.Address.PostalCode == "12345" && - options.SubscriptionDetails.Items.Count == 2 && - options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && - options.SubscriptionDetails.Items[0].Quantity == 1 && - options.SubscriptionDetails.Items[1].Price == plan.SecretsManager.StripeSeatPlanId && - options.SubscriptionDetails.Items[1].Quantity == 1 && - options.Coupon == StripeConstants.CouponIDs.SecretsManagerStandalone && - options.AutomaticTax.Enabled == true - )) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT0); - var taxAmount = result.AsT0; - Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); - } - - [Fact] - public async Task Run_NonUSWithoutTaxId_GetsTaxAmount() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "CA" && - options.CustomerDetails.Address.PostalCode == "12345" && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && - options.SubscriptionDetails.Items[0].Quantity == 1 && - options.AutomaticTax.Enabled == true - )) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT0); - var taxAmount = result.AsT0; - Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); - } - - [Fact] - public async Task Run_NonUSWithTaxId_GetsTaxAmount() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345", - TaxId = "123456789" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) - .Returns("ca_st"); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "CA" && - options.CustomerDetails.Address.PostalCode == "12345" && - options.CustomerDetails.TaxIds.Count == 1 && - options.CustomerDetails.TaxIds[0].Type == "ca_st" && - options.CustomerDetails.TaxIds[0].Value == "123456789" && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && - options.SubscriptionDetails.Items[0].Quantity == 1 && - options.AutomaticTax.Enabled == true - )) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT0); - var taxAmount = result.AsT0; - Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); - } - - [Fact] - public async Task Run_NonUSWithTaxId_UnknownTaxIdType_BadRequest() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345", - TaxId = "123456789" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) - .Returns((string)null); - - // Act - var result = await _command.Run(parameters); - - // Assert - Assert.True(result.IsT1); - var badRequest = result.AsT1; - Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response); - } - - [Fact] - public async Task Run_USBased_PersonalUse_SetsAutomaticTaxEnabled() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.FamiliesAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_USBased_BusinessUse_SetsAutomaticTaxEnabled() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.FamiliesAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_USBased_PersonalUse_DoesNotSetTaxExempt() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.FamiliesAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_USBased_BusinessUse_DoesNotSetTaxExempt() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "US", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - Assert.True(result.IsT0); - } - - [Fact] - public async Task Run_NonUSBased_PersonalUse_DoesNotSetTaxExempt() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.FamiliesAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - Assert.True(result.IsT0); - - } - - [Fact] - public async Task Run_NonUSBased_BusinessUse_SetsTaxExemptReverse() - { - // Arrange - var parameters = new OrganizationTrialParameters - { - PlanType = PlanType.EnterpriseAnnually, - ProductType = ProductType.PasswordManager, - TaxInformation = new TaxInformationDTO - { - Country = "CA", - PostalCode = "12345" - } - }; - - var plan = StaticStore.GetPlan(parameters.PlanType); - - _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); - - var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents - _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(expectedInvoice); - - // Act - var result = await _command.Run(parameters); - - // Assert - await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse - )); - Assert.True(result.IsT0); - } -} From e2f96be4dcf95935b899d75c25f06f1b63877c68 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:55:03 -0700 Subject: [PATCH 301/326] refactor(sso-config-tweaks): [Auth/PM-933] Make Single Sign-On URL required regardless of EntityId (#6314) Makes the Single Sign-On URL required regardless of the EntityId --- .../Request/OrganizationSsoRequestModel.cs | 3 +- src/Core/Resources/SharedResources.en.resx | 2 +- .../OrganizationSsoRequestModelTests.cs | 313 ++++++++++++++++++ 3 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 test/Api.Test/Auth/Models/Request/OrganizationSsoRequestModelTests.cs diff --git a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs index fcf386d7ee..349bdebb88 100644 --- a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs +++ b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs @@ -121,7 +121,7 @@ public class SsoConfigurationDataRequest : IValidatableObject new[] { nameof(IdpEntityId) }); } - if (!Uri.IsWellFormedUriString(IdpEntityId, UriKind.Absolute) && string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl)) + if (string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl)) { yield return new ValidationResult(i18nService.GetLocalizedHtmlString("IdpSingleSignOnServiceUrlValidationError"), new[] { nameof(IdpSingleSignOnServiceUrl) }); @@ -139,6 +139,7 @@ public class SsoConfigurationDataRequest : IValidatableObject new[] { nameof(IdpSingleLogoutServiceUrl) }); } + // TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028 if (!string.IsNullOrWhiteSpace(IdpX509PublicCert)) { // Validate the certificate is in a valid format diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 17b4489454..28ae70ca96 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -389,7 +389,7 @@ If SAML Binding Type is set to artifact, identity provider resolution service URL is required. - If Identity Provider Entity ID is not a URL, single sign on service URL is required. + Single sign on service URL is required. The configured authentication scheme is not valid: "{0}" diff --git a/test/Api.Test/Auth/Models/Request/OrganizationSsoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/OrganizationSsoRequestModelTests.cs new file mode 100644 index 0000000000..8348ba885d --- /dev/null +++ b/test/Api.Test/Auth/Models/Request/OrganizationSsoRequestModelTests.cs @@ -0,0 +1,313 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Api.Auth.Models.Request.Organizations; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Services; +using Bit.Core.Sso; +using Microsoft.Extensions.Localization; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Auth.Models.Request; + +public class OrganizationSsoRequestModelTests +{ + [Fact] + public void ToSsoConfig_WithOrganizationId_CreatesNewSsoConfig() + { + // Arrange + var organizationId = Guid.NewGuid(); + var model = new OrganizationSsoRequestModel + { + Enabled = true, + Identifier = "test-identifier", + Data = new SsoConfigurationDataRequest + { + ConfigType = SsoType.OpenIdConnect, + Authority = "https://example.com", + ClientId = "test-client", + ClientSecret = "test-secret" + } + }; + + // Act + var result = model.ToSsoConfig(organizationId); + + // Assert + Assert.NotNull(result); + Assert.Equal(organizationId, result.OrganizationId); + Assert.True(result.Enabled); + } + + [Fact] + public void ToSsoConfig_WithExistingConfig_UpdatesExistingConfig() + { + // Arrange + var organizationId = Guid.NewGuid(); + var existingConfig = new SsoConfig + { + Id = 1, + OrganizationId = organizationId, + Enabled = false + }; + + var model = new OrganizationSsoRequestModel + { + Enabled = true, + Identifier = "updated-identifier", + Data = new SsoConfigurationDataRequest + { + ConfigType = SsoType.Saml2, + IdpEntityId = "test-entity", + IdpSingleSignOnServiceUrl = "https://sso.example.com" + } + }; + + // Act + var result = model.ToSsoConfig(existingConfig); + + // Assert + Assert.Same(existingConfig, result); + Assert.Equal(organizationId, result.OrganizationId); + Assert.True(result.Enabled); + } +} + +public class SsoConfigurationDataRequestTests +{ + private readonly TestI18nService _i18nService; + private readonly ValidationContext _validationContext; + + public SsoConfigurationDataRequestTests() + { + _i18nService = new TestI18nService(); + var serviceProvider = Substitute.For(); + serviceProvider.GetService(typeof(II18nService)).Returns(_i18nService); + _validationContext = new ValidationContext(new object(), serviceProvider, null); + } + + [Fact] + public void ToConfigurationData_MapsProperties() + { + // Arrange + var model = new SsoConfigurationDataRequest + { + ConfigType = SsoType.OpenIdConnect, + MemberDecryptionType = MemberDecryptionType.KeyConnector, + Authority = "https://authority.example.com", + ClientId = "test-client-id", + ClientSecret = "test-client-secret", + IdpX509PublicCert = "-----BEGIN CERTIFICATE-----\nMIIC...test\n-----END CERTIFICATE-----", + SpOutboundSigningAlgorithm = null // Test default + }; + + // Act + var result = model.ToConfigurationData(); + + // Assert + Assert.Equal(SsoType.OpenIdConnect, result.ConfigType); + Assert.Equal(MemberDecryptionType.KeyConnector, result.MemberDecryptionType); + Assert.Equal("https://authority.example.com", result.Authority); + Assert.Equal("test-client-id", result.ClientId); + Assert.Equal("test-client-secret", result.ClientSecret); + Assert.Equal("MIIC...test", result.IdpX509PublicCert); // PEM headers stripped + Assert.Equal(SamlSigningAlgorithms.Sha256, result.SpOutboundSigningAlgorithm); // Default applied + Assert.Null(result.IdpArtifactResolutionServiceUrl); // Always null + } + + [Fact] + public void KeyConnectorEnabled_Setter_UpdatesMemberDecryptionType() + { + // Arrange + var model = new SsoConfigurationDataRequest(); + + // Act & Assert +#pragma warning disable CS0618 // Type or member is obsolete + model.KeyConnectorEnabled = true; + Assert.Equal(MemberDecryptionType.KeyConnector, model.MemberDecryptionType); + + model.KeyConnectorEnabled = false; + Assert.Equal(MemberDecryptionType.MasterPassword, model.MemberDecryptionType); +#pragma warning restore CS0618 // Type or member is obsolete + } + + // Validation Tests + [Fact] + public void Validate_OpenIdConnect_ValidData_NoErrors() + { + // Arrange + var model = new SsoConfigurationDataRequest + { + ConfigType = SsoType.OpenIdConnect, + Authority = "https://example.com", + ClientId = "test-client", + ClientSecret = "test-secret" + }; + + // Act + var results = model.Validate(_validationContext).ToList(); + + // Assert + Assert.Empty(results); + } + + [Theory] + [InlineData("", "test-client", "test-secret", "AuthorityValidationError")] + [InlineData("https://example.com", "", "test-secret", "ClientIdValidationError")] + [InlineData("https://example.com", "test-client", "", "ClientSecretValidationError")] + public void Validate_OpenIdConnect_MissingRequiredFields_ReturnsErrors(string authority, string clientId, string clientSecret, string expectedError) + { + // Arrange + var model = new SsoConfigurationDataRequest + { + ConfigType = SsoType.OpenIdConnect, + Authority = authority, + ClientId = clientId, + ClientSecret = clientSecret + }; + + // Act + var results = model.Validate(_validationContext).ToList(); + + // Assert + Assert.Single(results); + Assert.Equal(expectedError, results[0].ErrorMessage); + } + + [Fact] + public void Validate_Saml2_ValidData_NoErrors() + { + // Arrange + var model = new SsoConfigurationDataRequest + { + ConfigType = SsoType.Saml2, + IdpEntityId = "https://idp.example.com", + IdpSingleSignOnServiceUrl = "https://sso.example.com", + IdpSingleLogoutServiceUrl = "https://logout.example.com" + }; + + // Act + var results = model.Validate(_validationContext).ToList(); + + // Assert + Assert.Empty(results); + } + + [Theory] + [InlineData("", "https://sso.example.com", "IdpEntityIdValidationError")] + [InlineData("not-a-valid-uri", "", "IdpSingleSignOnServiceUrlValidationError")] + public void Validate_Saml2_MissingRequiredFields_ReturnsErrors(string entityId, string signOnUrl, string expectedError) + { + // Arrange + var model = new SsoConfigurationDataRequest + { + ConfigType = SsoType.Saml2, + IdpEntityId = entityId, + IdpSingleSignOnServiceUrl = signOnUrl + }; + + // Act + var results = model.Validate(_validationContext).ToList(); + + // Assert + Assert.Contains(results, r => r.ErrorMessage == expectedError); + } + + [Theory] + [InlineData("not-a-url")] + [InlineData("ftp://example.com")] + [InlineData("https://example.com